JUC多线程并发编程(一)
目录
什么是JUC?
JUC是java.util.concurrent的简写,官方只是用它作为一种命名,多线程编程主要是用这个包下里的类,称为JUC多线程编程。
传统的Sychronized锁
以下代码,在多线程并发的情况下,会存在问题,输出的结果会非常的乱。
public static void main(String[] args) {
Num num = new Num();
new Thread(()->{
for (int i =1;i<=50;i++){
num.decrement();
}
},"a").start();
new Thread(()->{
for (int i =1;i<=50;i++){
num.decrement();
}
},"b").start();
new Thread(()->{
for (int i =1;i<=50;i++){
num.decrement();
}
},"c").start();
}
public static class Num{
private int number=40;
public void decrement(){
if (number>0) {
number--;
System.out.println(Thread.currentThread().getName()+" "+number);
}
}
传统的解决方法呢?就是在我们方法上加一个Synchronized关键字
public Sychronized void decrement(){
if (number>0) {
number--;
System.out.println(Thread.currentThread().getName()+" "+number);
}
Synchronized的作用:当多个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法,但是可以访问非synchronized修饰的方法。
lock锁
Lock所是一个接口,其所有的实现类为
ReentrantLock(可重入锁)
ReentrantReadWriteLock.ReadLock(可重入读写锁的读锁)
ReentrantReadWriteLock.WriteLock(可重入读写锁的写锁)
用lock锁解决上述问题:
ReentrantLock lock=new ReentrantLock()
public void decrement(){
try{
if (number>0) {
lock.lock();
number--;
System.out.println(Thread.currentThread().getName()+" "+number);
}catch(exception e){
e.printStack();
}finally{
lock.unlock();
}
}
lock锁和Synchronizd锁的作用是一样,没有什么区别,只是换了一种形式,但是这两者的不同后面再说
可重入锁:如果锁具备可重入性,则称为可(可以)重(再次)入(进入同步域,即同步代码块/方法)锁(同步锁)。可重入就是指某个线程已经获得某个锁,可以再次获取相同的锁而不会出现死锁。也就是说当一个类里面有同步方法A,同步方法A又实现了同步方法B,这时一个线程调用同步方法A,获取锁,当执行了到方法B时,会再次获取锁,获取的时同一把锁,这样就叫可重入锁,能有效的防止死锁。
公平锁:指的是非常公平,线程会依次获取锁,顺序执行。
非公平锁:指的是会出现插队现象,某些线程会直接先执行。
Synchronized和Lock的区别
Synchronizd:
1.是Java类中的一个内置关键字
2.无法获取锁的状态,也就是不能这个锁是否被占有和空闲
3.自动释放锁
4.线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着
5.是不可中断的、非公平的、可重入锁
6.适合锁少量的同步代码
7.有代码块锁和方法锁
Lock:
1.是java的一个类
2.可判断是否获取了锁
3.需手动释放锁,如果不释放会造成死锁
4.线程一在获得锁的情况下阻塞了,可以使用tryLock()尝试获取锁
5.非公平的、可判断的、可重入锁
6.适合锁大量的同步代码
7.只有代码块锁
8.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(拥有更多的子类)
传统的生产者与消费者问题
public static void main(String[] args) {
Num num = new Num();
new Thread(()->{
for (int i=1;i<=10;i++){
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i=1;i<=10;i++){
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
class Num{
private int count=0;
public synchronized void increment() throws InterruptedException {
if (count!=0){
this.wait();
}
count++;
this.notifyAll();
System.out.println(Thread.currentThread().getName()+" "+ count);
}
public synchronized void decrement() throws InterruptedException {
if (count==0){
this.wait();
}
count--;
this.notifyAll();
System.out.println(Thread.currentThread().getName()+" "+ count);
}
}
生产者与消费者问题属于是线程之间的通信。
线程之间的通信主要有:
wait():指的是会让当前线程进入休眠状态,但不是阻塞状态
notify():会唤醒当前对象上一个休眠状态的线程
notifyAll():会唤醒当前对象的所有线程,会按照线程的优先级调度执行
角色:
生产者:负责生产产品的线程
消费者:负责消费的产品的线程
解决线程之间通信的方法有:
1.管程法:
指的是,会创建一个容器类,里面存入一个缓冲区数组,主要用来存储产品的数量,然后再通过判断产品数量的大小来确保线程之间的通信
2.信号灯法:
指的是,在产品类里去设置一个信号flag,当信号变化的时候,就可以通知和唤醒线程,确保线程之间的通信。
在生产者与消费者通信的过程主要分为:判断等待,业务,唤醒,而在多线程的情况下(两个线程以上),如果你用的是if来判断等待,会存一个虚假唤醒的情况。什么是虚假唤醒呢?虚假唤醒指的是当有多个线程阻塞后,被其中一个线程唤醒,此时只有一个线程是在作用有效范围内的,其他线程都是多余的,这时会造成线程安全问题。
那为什么用if判断就会出现虚假唤醒呢?拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。所以说用while判断能有效的解决虚假唤醒的问题。
public synchronized void increment() throws InterruptedException {
while (count!=0){
this.wait();
}
count++;
this.notifyAll();
System.out.println(Thread.currentThread().getName()+" "+ count);
}
public synchronized void decrement() throws InterruptedException {
while (count==0){
this.wait();
}
count--;
this.notifyAll();
System.out.println(Thread.currentThread().getName()+" "+ count);
}
lock版的生产者与消费者问题
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
try {
lock.lock();
while (count!=0){
condition.await();
}
count++;
condition.signal();
System.out.println(Thread.currentThread().getName()+" "+ count);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
try {
lock.lock();
while (count==0){
condition.await();
}
count--;
condition.signal();
System.out.println(Thread.currentThread().getName()+" "+ count);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
lock版的线程之间的通信:
通过lock.newCondition产生一个condition对象
1.用condition.await()方法替代传统版的wait()方法,进入休眠状态
2.用condition.signal()方法替代传统版的notifyAll()方法,唤醒线程
两者之间作用都是相同的,差别不大,但是lock版的总会有好处,如果有这样的一个需要,需要按自定义来唤醒线程,此时lock的condition就能实现精准唤醒,而传统版的是无法实现的。可以通过lock产生多个condition对象,每一个condition对象对应一个线程,此时我们可以在当前线程在唤醒的时候,可以指定一个condition来唤醒,这样就实现了按自定义顺序来唤醒线程.
8锁现象
1.当有两个实例方法加上synchronized关键字,同一个对象,有两个线程a,b(a的代码在b前)去执行两个实例方法,此时无论无何都会先执行a线程调用的实例方法,因为a线程首先拿到锁,谁先拿到锁就会先执行
2.让线程a睡3秒,结果会依然是先执行a线程调用的实例方法
3.加上一个普通方法,线程b实现普通方法,此时的结果会是,由于a睡3秒,先执行就是普通方法,也就是说普通方法不受锁的影响
4.不同对象分别实现两个实例方法,此时的结果将是睡3秒的线程之后执行,线程b先执行
5.当实例方法变为静态方法后,锁的就是class类模板,结果依然是线程a先执行
6.实例方法变为静态方法并用不同对象,结果依然是线程a先执行
7.当一个实例方法一个静态方法,让同一个对象去调用,结果是线程b先执行,因为这两者的锁不同,互相不影响
8.当一个实例方法一个静态方法,让不同对象去调用,结果是线程b先执行,一个锁模板,一个锁对象,自然就不影响
不安全的集合类
list
为什么list是线程不安全的?在单线程是安全的,当多线程同时对list使用add()方法,此时就会出现并发修改异常(ConcurrentModificationException)
那怎么解决这个问题呢?
1.用vector来替代list,它是内部加锁,是一个线程安全类,底层的add方法使用了synchronized关键字。
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
2.用Collections.synchronizedList(list)包装list,也是内部加锁,能让list变得多线程安全
3.使用juc里的CopyOnWriteArrayList替代list(推荐使用)写入时复制,读写分离的思想。源码如下,使用的是lock锁
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
这样写时复制实现了读写分离,我们不需要在读的时候加锁(之前读需要加锁是因为读写不能同时进行,但一旦给读加了锁,那么读也不能同时进行,就降低了并发效率)
但是,我们每“写入”一个元素就要复制扩容一次数组,是非常耗时耗资源的,所以当我们需要写入较多数据的时候,CopyOnArrayList就不那么合适了。
set
set集合在多线程的情况下,使用add方法也是不安全,也会报并发修改异常异常
将不安全的集合变成安全集合的方法:
1.Set set = Collections.synchronizedSet(new HashSet<>());
2.Set set = new CopyOnWriteArrayListSet<>();
map
也是不安全的集合类
变成安全的方法:
1.Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
无论读取还是写入,都会进行加锁,当并发级别特别高,线程之间在任何操作上都会进行等待,效率低。
2.Map<String,String> map = new ConcurrentHashMap<>();
采用分段锁技术,其中Segment继承于ReentrantLock。不会像HashTable(线程安全) 那样不管是put还是get操作都需要做同步处理,理论上ConcurrentHashMap支持CurrentLevel(Segment数组数量)的线程并发。每当一个线程占用锁访问一个Segment时,不会影响其他的Segment.
callable
callable和runnable类似,都是线程执行的接口,但callable有:
1.使用call方法,有返回值,使用futureTask.get()方法获得,但会阻塞,一般放到程序的最后
2.可以抛出异常
3.Callable与Future结合,实现利用Future来跟踪异步计算的结果
**new Thread(new Runnable())**这是一种形式,**new Thread(new FutureTask())**这种形式,**new FutureTask()**里面包含的参数是一个实现callable接口的实现类,线程的启动,需要借助我们FutrueTask来启动线程.
异步计算:
用不同的线程去做不同的事,最后再进行数据汇总并返回结果。是分布式计算的一种实现方式,开发人员不必花费太多的硬件成本,即可通过软件方式以更低的成本实现大规模运算需要的处理能力。
在一些业务比较复杂,某些方法计算比较耗时的时候单线程就无法快速返回结果,用户响应时间变长,这个时候就要使用异步计算来加快数据的处理。
over
本文借助于狂神说Java:bilibili.com/video/BV1B7411L7tE?p=14加上自己的理解做了一份笔记.