一、为什么使用多线程?
因cpu、内存、磁盘IO的速度差异过大,为了充分利用cpu资源。
二、带来的问题
线程不安全,得到预料之外的结果
三、不安全案例
/*** * 假设创建100个线程对变量 count 进行 +1 操作,100个之行完的结果有可能小于100 * **/
public int count =0;
public void add(){
count ++;
}
public static void main(String[] args) throws InterruptedException {
JUCTest jucTest = new JUCTest();
CountDownLatch countDownLatch = new CountDownLatch(100);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executorService.execute(()->{
jucTest.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(jucTest.count);
}
得出的结果小于100;
/*** * 单例模式下获取对象,多个线程同时获取实例,也会存在不同的对象 * */
public class Data {
private Data(){}
/***
* 单例模式下获取对象,多个线程同时获取实例,也会存在不同的对象
* */
private static Data instance = null;
public static Data getInstance(){
if(instance == null){
instance = new Data();
}
return instance;
}
}
//测试类
public static void main(String[] args) throws InterruptedException {
JUCTest jucTest = new JUCTest();
CountDownLatch countDownLatch = new CountDownLatch(100);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(()->{
Data instance = Data.getInstance();
System.out.println(instance.hashCode());
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(jucTest.count);
}
执行结果中存在不同哈希值的对象
改进:加锁
public class Data {
private Data(){}
/***
* 单例模式下获取对象,多个线程同时获取实例,也会存在不同的对象
* */
private static Data instance = null;
public static Data getInstance(){
synchronized (Data.class){
if(instance == null){
instance = new Data();
}
}
return instance;
}
}
加锁出现的问题:
因有指令重排序的问题,故单独加锁存在不存在的实例情况。
创建对象的过程:申请空间、初始化、引用指向空间地址(三个步骤的指令可能重排序),故需要增加可见性 volatile 关键字
public class Data {
private Data(){}
/***
* 单例模式下获取对象,多个线程同时获取实例,也会存在不同的对象
* */
//volatile 防止指令重排序
private static volatile Data instance = null;
public static Data getInstance(){
synchronized (Data.class){
if(instance == null){
instance = new Data();
}
}
return instance;
}
}
四、多线程创建方式
实现Runnable接口、Callable接口、继承Thread类、线程池
五、线程池实现的不同方式
如下述代码
ExecutorService executorService = Executors.newCachedThreadPool(); //带缓存的线程池,默认核心线程数0,最大线程数为Integer的最大值
ExecutorService executorService1 = Executors.newFixedThreadPool(10); //创建固定大小线程池
ExecutorService executorService2 = Executors.newSingleThreadExecutor();//这个线程池只有一个线程,方法可以创建一次执行单个任务的执行程序.线程出现异常,它会重新创建一个线程代替挂掉的线程。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);//含有任务调度的线程池
/***
* 实际工作中,我们更多使用如下方式,来指定线程池参数,实际上上述四种方式均以父类ThreadPoolExecutor的方式创建
* */
/** 参数说明:核心线程,最大线程,最大线程存活时间,时间单位,阻塞队列 */
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(
10,
20,
10,
TimeUnit.DAYS,
new LinkedBlockingQueue<>());//
/*** 处理流程
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满,若线程数小于最大线程数,创建线程。
若线程数等于最大线程数,则执行拒绝策略 */
}
六、execute和submit
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool(); //带缓存的线程池,默认核心线程数0,最大线程数为Integer的最大值
executorService.execute(()->{}); //没有返回值
Future<?> submit = executorService.submit(() -> {
}); //能返回Future对象,通过get得到返回结果
}
七、synchronized和Lock
/**** * 均能实现对应的锁功能 * synchronized:Java关键字,不公平,无需手动释放, * Lock: 接口,是否公平可自定义,需要手动释放,要不然会发生死锁,Lock还可以通过多个condition控制 *锁的程度:无锁(cas)、偏向锁、自旋锁、重量级锁 * */