Java多线程常用方式
1.volatile 关键字
volatile修饰的变量保持内存可见性和防止指令重排序,就是任意一个线程修改了值,会马上同步到别的线程中,但是不保证非原子操作的一致性,比如 i++ 拆分执行是 先读取 然后修改 最后赋值
指令重排序就是编译器的一种优化手段,可能实际执行的顺序和我们编写的代码顺序不一致,有时候就会导致一些问题,比如两个线程,一个线程负责加载资源,加载完成后就将某个变量的值改为true,而另一个线程循环判断这个变量是否为true
如果为true就代表资源已经加载完成,可以进行别的操作,如果指令重排序了,有可能先执行了将变量值改为true,这就会导致还没有加载资源,而另一个线程发现为true以为加载资源完成了 进行操作就会引发异常
2.原子类
在 java.util.concurrent.atomic 包下有很多原子类 比如AtomicInteger , 这个其实是在内部维护了一个以volatile修饰的int 变量,可以和使用Integer类一样使用,但是对这个类的非原子操作 比如++ 就会是线程安全的
这个只能用来做一个计数器,在多个线程操作这个值,比如都进行++操作 最终值也会是正确的,但是不能用来作为逻辑判断
常用方法有: incrementAndGet() 当前值+1 相当于i ++操作 ,decrementAndGet() 当前值-1 相当于 i- -操作
3.CountDownLatch
可以等待一些线程执行完毕以后再继续执行,举例一个应用场景就是 一个超市,每天要计算各种不同商品类型的销售数量和销售金额,计算完每一种商品销售的数量和金额以后要进行一个汇总,得到当天总共销售了多少商品,销售金额是多少
这时候可以开启多个线程去计算每个商品类型的销售数量和金额,当所有类型的计算完毕后,在进行汇总计算.
内部是通过一个计数器来实现的,初始值就是要执行任务的线程的数量,每一个线程执行完毕该值-1,当值为0时就代表所有线程已执行完毕,所以在实例化这个对象的时候 必须要传入线程的数量
常用方法: countDown() 内部计数器值-1,一般放在线程执行任务完毕时调用,代表线程执行完毕.
await() 调用这个方法的线程会阻塞,等待计数器值为0,也就是所有线程执行完毕,一般是主线程开启别的线程进行计算,然后调用这个方法,阻塞等待别的线程执行
,还有一个重载方法await(long timeout, TimeUnit unit
) 这个方法只是多了可以设置一个最长等待时间,第一个参是最长等待时间,第二个参是等待时间单位,都会抛出一个InterruptedException(线程中断异常)
4.FutureTask<V>
unnable接口的run()方法是没有返回值的并且也不能抛出异常,所以在jdk1.5的时候新增了Callable<V>接口,可以返回值和抛出异常 泛型代表了返回值,但是由于启动线程必须要new一个Thread对象调用start()方法,而Thread对象只能接收Runable接口,
所以就需要new一个FutureTask<V>的对象,这个对象可以传入Callable接口,而这个FutureTask<V> 这个类也实现了 Runable接口,所以就可以传入Thread类的构造方法,然后调用start()启动线程,线程启动以后要获得返回值需要调用这个FutureTask<V>对象的get()方法,调用get()方法获取返回值,而线程还未执行完毕,那么会阻塞当前调用这个方法的线程,直到哪一个线程执行完毕返回值了以后,当前调用线程才会继续执行.这个方法也有一个重载方法get(long timeout, TimeUnit unit) 第一个参是最长等待时间,第二个参是等待时间单位 都会抛出InterruptedException(中断异常)和ExecutionException(执行异常)
5.synchronized
java提供的一个关键字,用于解决多线程的安全问题,也就是多个线程操作一个同一个数据(变量)成为共享数据会引发问题.
举例比如卖电影票,电影票的数量就是一个共享数据,多个线程同时卖票,代码逻辑肯定要判断当前票的余额是否大于0, 大于0才能减少票的数量,如果不进行同步,假如当前只剩下1张票了,两个线程同时执行这个方法,因为当前票的数量大于0,
就会执行卖票的逻辑,只有1张票,两个线程都进行卖票肯定会出现票的数量变成-1,这样显然就不正确了.
5.1 同步代码块
1 synchronize(同步监视器){ 2 //同步代码 3 }
重点在于这个同步监视器也称为锁,这个锁只有一个要求就是对象就行,其实还有一个隐含的要求就是多个线程执行的同步代码块中的这个锁必须要是同一个,才能实现,任何一个时刻只有一个线程在执行同步代码
5.2同步方法
1 public synchronized void test1(){ 2 //代码 3 } 4 5 6 public static synchronized void test2(){ 7 //代码 8 }
这两个分别是实例同步方法和静态同步方法,其实这两个的区别就在于锁的对象不同,实例方法锁是this,而静态方法锁是当前类的class对象,这个对象在一个类加载器中都是唯一的
在同步代码中出现问题时要首先排查锁对象是否是同一个,如果不是就会造成线程安全问题.
5.3 线程通信
线程通信就是多个线程之间可以让自己阻塞,将别的线程唤醒,达到一些逻辑,比如两个线程交替输出一个值,就好像两个线程在沟通
主要使用Object类的三个方法 wait(),notity(),notifyAll() 这三个方法作用我就不多说了,应该都懂,重点在于这三个方法必须要做在同步代码块或同步方法中使用
还有一点就是调用这三个方法的对象必须是锁对象,是别的对象调用会抛出异常.
6.Lock
在jdk1.5以前,实现同步只能使用synchronized,但是操作起来没那么方便,所以就提供了Lock接口,可以显式的操作上锁和解锁,而且想实现线程通信不需要在同步代码块或方法中,
不过和synchronized相同的是,多个线程执行同步方法中的Lock对象也必须是同一个.
常用方法: lock() 上锁 在这个方法以后的代码都是同步的, unlock() 解锁 建议放在finally中,无论是否异常都会解锁
newCondition() 返回一个Condition接口的对象,用于进行线程通信有三个方法await()和signal()以及signalAll()分别对应Object类的那三个方法
不过这个对象很灵活,比如一个线程调用Condition对象的await()方法进行阻塞,那么在别的地方也只要调用这个对象的signal()或signalAll()方法唤醒即可
6.1 ReentrantLock(可重入锁)
这个是Lock接口的一个实现类,一般也用这个,是一个互斥,一个线程可重复进入的锁,可以创建非公平和公平两种方式,构造方法传false或者不传是非公平,传true就是公平锁
非公平锁,当一个线程执行完毕,会在等待执行该方法中的线程随机选择一个来获得锁,执行代码,很有可能这个线程刚执行完,又继续获得锁,继续执行
公平锁, 每一次会让在当前方法上等待最久的线程获得锁并执行,这样就实现了所有线程顺序执行.但是性能肯定没有非公平锁高.
6.2 ReadWriteLock(读写锁)
这个接口是Lock接口的子接口,主要用于访问共享资源,如果两个线程同时写会有问题,两个线程一个读,一个写会有问题,但如果多个线程同时读是没有问题的,读写锁就是解决这个场景
ReentrantReadWriteLock(可重入读写锁) 也和重入锁一样,可以创建非公平和公平两种模式,通过readLock()方法可以得到一个读锁对象,相应的上锁,解锁方法是一样的
通过writeLock()方法可以得到一个写锁对象,解锁上锁方法也一样.重点在于使用读锁的方法,多个线程同时执行,而写锁是互斥的,只允许一个线程执行,并且一个线程在写的时候,
其他读锁的线程不能进行读取,只有当没有线程进行写的时候,读锁线程才能读取.这也就可以应用于一些特殊的场景.读多,写少,可以大大提升效率.
7.线程池
线程池其实和jdbc那个连接池差不多,因为创建销毁线程也是挺占用资源的,线程池固定持有一些线程对象,我们只需要传入要执行的代码或者说是任务,循环利用线程,可以节省不少资源
Jdk推荐创建线程使用Executors工具类来创建线程池,主要有以下五种方法 可以创建五种不同的线程池
newFixedThreadPool() | 创建固定大小的线程池 |
newSingleThreadExecutor() | 创建只有一个线程的线程池 |
newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
newScheduledThreadPool() | 创建一个固定大小的可以延时执行的线程池 |
newSingleThreadScheduledExecutor() |
创建一个只有一个线程可以延时执行的线程池 |
其中前三个非延时执行线程池都会返回一个ExecutorService接口的类型,实现类是ThreadPoolExecutor,常用方法如下:
execute() | 这个方法只能传入Runable接口类型 |
submit() | 这个方法可以传入Runable或者Callable,返回值是future接口类型,调用get()方法即可获取返回值 |
shutdown() |
关闭线程池 |
后面两个延时执行的线程池返回ScheduledExecutorService接口类型,实现类是ScheduledThreadPoolExecutor,常用方法和上面的一样.
7.1 线程池安全问题
由于Executors工具类的方法创建线程池,本质上也就是调用对应实现类的构造方法,只是屏蔽了一些细节,但是创建线程池没有设置最大上限,和一些别的问题,
可能会导致OOM(内存溢出)或者一些别的问题,阿里巴巴开发规范也推荐建议手动创建ThreadPoolExecutor对象来使用.我列举下构造方法有哪些参数
corePoolSize | 核心线程池大小(一定会保留的线程数,无论是否空闲) |
maximumPoolSize | (当核心线程无空闲,会增加新的线程,最大允许增加到多少个) |
keepAliveTime | 线程最大空闲时间(超出核心线程数量的线程允许空闲多久,超出时间会销毁该线程) |
unit | 时间单位 是一个枚举类 用来设置空闲时间的单位 |
workQueue | 线程等待队列(等待执行的线程都在此队列) 一般new ArrayBlockingQueue<>(队列长度) 即可 |
threadFactory | 非必须参数,线程创建工厂(可以对线程池每一个创建的线程对象进行一些设置 |
handler | 非必须参数,拒绝策略 (当等待队列已满,提交的任务会怎么样进行反馈拒绝) |
8. 总结
这是我这几天学习的关于多线程的知识,记录一下,如有不对的问题,欢迎指正.