其实,很多初学者(包括我自己)初期学习多线程时都被视频带偏了...虽然我始终认为培训班的视频是最适合非科班零基础入门的,但是在多线程方面,无一例外都讲得比较糟糕。
感触很深的一点是:很多新手觉得多线程难,并不是因为volatile、ReentrantLock或者Executor线程池,而是从一开始就没弄明白“什么是锁”,导致后面根本学不进去。
- 什么是“锁”?
- 锁到底长啥样?
- 它锁定的是代码吗?
在我看来,这个问题不搞清楚,后面的内容根本学不明白。而一旦搞清楚这些概念,后面很多问题其实也就迎刃而解。
内容介绍:
- 线程安全问题与解决办法
- 锁到底长啥样
- 关于锁的几个案例
- 面试题:写一个固定容量的同步容器
线程安全问题与解决办法
在上一篇结尾,我们说Java两种创建多线程的方法中,一般推荐实现Runnable接口的方式。主要原因可以归结为:
- 资源和线程分离,更加面向对象
- 可以做到资源共享
而所谓的线程安全问题可以粗浅地理解为“数据不一致”。但单纯的资源共享并不一定会导致线程安全问题。当同时满足以下三个条件时,才可能引发线程安全问题。
- 多线程环境
- 有共享数据
- 有多条语句操作共享数据/单条语句本身非原子操作
来看一段
@养兔子的大叔 在(JDK)ReetrantLock手撕AQS一文中关于线程安全的示例代码:
public class ThreadForIncrease {
static int cnt = 0; //共享数据cnt
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
//有多条语句操作共享数据
int n = 10000;
while(n>0){
cnt++;
n--;
}
}
};
//多线程环境
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
Thread t4 = new Thread(r);
Thread t5 = new Thread(r);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
try {
//等待足够长的时间 确保上述线程均执行完毕
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(cnt);
}
}
//输出的结果会小于50000
很明显,上面示例完全符合“线程安全问题”的三个条件。
出现问题的原因在于cnt++并不是原子性操作,实际上分三步:
- 各个线程从主存拷贝变量
- 在自己的工作内存进行+1操作
- 把结果回写到主存
如何解决?仔细回想一下三个条件:
- 多线程环境(这个是前提,无法改变,没有多线程当然没有安全问题)
- 有共享数据(通常无法改变,特定情境下必须要操作共享数据)
- 非原子性操作(可以改变!)
所以经过分析,我们能优化的只有第三点:把对共享数据的操作变成原子性操作。针对上面的情况解决办法有多种,比如cnt使用原子类AutomicInteger,或者加锁等等。这里演示加锁的情况(其实这种情况加锁有点下药过猛了)。
//使用synchronized实现多线程累加操作
public class synchronizedForIncrease {
static int cnt = 0;
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public synchronized void run() {//同步方法(synchronized加锁)
int n = 10000;
while(n>0){
cnt++;
n--;
}
}
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
Thread t4 = new Thread(r);
Thread t5 = new Thread(r);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
try {
//等待足够长的时间 确保上述线程均执行完毕
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(cnt);
}
}
//输出结果将和预想中的一致:50000
用synchronized修饰run()方法后,就相当于将方法内的多个语句捆绑在一起,要么全部执行,要么尚未开始,不会出现“执行到一半被挂起”的情况,也就避免了线程安全问题的发生。
锁到底长啥样
其实“锁”本身是个对象,且理论上可以是任意对象。synchronized这个关键字不是“锁”,硬要说的话,加synchronized仅仅是相当于“加锁”这个操作,真正的锁是“某一个对象”。
所以,所谓的加锁,严格意义上不是锁住代码块!如果这样想的话,后面很多问题就没法解释了。
补充几个概念:
- 互斥的最基本条件是:共用同一把锁
- 静态方法的锁是所在类的字节码对象:xxx.class对象,普通方法的锁是this对象
- 针对同一个线程,synchronized锁是可重入的
下面通过几个小案例,帮大家加深对上面三句话的理解
关于锁的几个案例
- 同一个类中的synchronized method m1和method m2互斥吗?
- 同一个类中synchronized method m1中可以调用synchronized method m2吗?
- 子类同步方法synchronized method m可以调用父类的synchronized method m吗(super.m())?
- 静态同步方法和非静态同步方法互斥吗?
面试题:写一个固定容量的同步容器
据说是淘宝?很久以前的一道面试题:
面试题:写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用
wait/notifyAll实现:
public class MyContainer1<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //固定容量,假定最多10个元素
private int count = 0;
//put方法
public synchronized void put(T t) {
while(lists.size() == MAX) { //想想为什么用while而不是用if?
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
this.notifyAll(); //通知消费者线程进行消费
}
//get方法
public synchronized T get() {
T t = null;
while(lists.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.removeFirst();
count --;
this.notifyAll(); //通知生产者进行生产
return t;
}
public static void main(String[] args) {
MyContainer1<String> c = new MyContainer1<>();
//启动消费者线程
for(int i=0; i<10; i++) {
new Thread(()->{
for(int j=0; j<5; j++)
System.out.println(c.get());
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for(int i=0; i<2; i++) {
new Thread(()->{
for(int j=0; j<25; j++)
c.put(Thread.currentThread().getName() + " " + j);
}, "p" + i).start();
}
}
}
对于初学者,这个面试题的难点在于:
首先,能想到在MyContainer中塞入LinkedList作为容器(因为有removeFirst方法,比较方便)。Java集合体系中,已经提供了足够多的容器,我们如果要模拟自己的容器,一般会选择将现有的容器包装进自己的容器中,而不是去自己实现一个容器。
其次,wait方法必须配合notifyAll。据说《Effective Java》甚至提出,wait在绝大多数场景下应该伴随着notifyAll而不是notify。因为notify的唤醒是随机,不能确定唤醒的是哪个线程(可能是消费者方,也可能是生产者方)。所以当某个生产者线程生产完第10个商品让出执行权后,下次抢到执行权的可能还是生产者方的其他线程(触发lists.size()==MAX条件),这样全部生产者线程就会等待(在此之前消费者线程也已经全部等待),整个程序就会发生死锁:
第⑤步只是举个例子,实际上也有可能是唤醒消费者,因为notify的唤醒是随机的如果还是有同学不明白为什么生产者线程最终会全部等待,可以看看下面的例子,虽然不够贴切,但是以我的美术功底,尽力了:
如果是notifyAll,则会唤醒所有线程,且各个线程抢到执行权的概率是一致的。即使下一次还是生产者线程抢到执行权并且等待了,此时还有其他线程是活着的。
最后,由于理论上锁可以是任意对象,所以锁的wait/notify/notifyAll等方法就被定义在Object类中,让所有类去继承。如果你仍觉得synchronized才是锁,这个问题是解释不通的。所以,请明确,wait/notify/notifyAll这些方法都是锁对象的方法,线程之所以会产生等待、唤醒等一系列状态,都是去读取锁对象时被指定的。
wait notify notifyAll
最后,提供ReentrantLock实现的版本,更为简单,而且可以精确唤醒生产者线程/消费者线程:
public class MyContainer2<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put(T t) {
try {
lock.lock();
while(lists.size() == MAX) { //想想为什么用while而不是用if?
producer.await();
}
lists.add(t);
++count;
consumer.signalAll(); //通知消费者线程进行消费
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
while(lists.size() == 0) {
consumer.await();
}
t = lists.removeFirst();
count --;
producer.signalAll(); //通知生产者进行生产
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
MyContainer2<String> c = new MyContainer2<>();
//启动消费者线程
for(int i=0; i<10; i++) {
new Thread(()->{
for(int j=0; j<5; j++) System.out.println(c.get());
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for(int i=0; i<2; i++) {
new Thread(()->{
for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
}, "p" + i).start();
}
}
}
按照惯例,还是留一道思考题,是我之前面试被考到的,咋一听有点懵,其实本质是一样的。始终抓住锁的本质即可迎刃而解:
一个对象的get/set方法如果加上synchronized,t1访问get方法,t2访问set方法,这两个线程互斥吗?标签:Thread,synchronized,int,线程,new,多线程,public From: https://www.cnblogs.com/jiaodaoniujava/p/17636969.html