synchronize
案例
以售票问题为例,假设这里存在三个线程用来买票,则代码如下:
public class SellTicket implements Runnable {
//定义一个成员变量表示有100张票
private int tickets=100;
public void run(){
while (true){
if(tickets>0){
try {
//通过sleep()方法来等待
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets--+"张票");
}else{
//System.out.println("");
}
}
}
}
@SuppressWarnings("all")
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
如果运行上面代码,会出现下面两种异常情况:
- 相同票数出现多次;
- 出现了负票
原因分析
- 出现相同票数的原因
- 出现负数的原因
- 总结:出票操作、更新票数操作以及输出操作没有整合在一起,随时可能被其他线程打断。
同步代码块
判断多线程程序是否具有安全问题的标准:
- 是否有多线程坏境
- 是否有共享数据
- 是否有多条语句操作共享数据
解决多线程问题的方法:
利用 synchronized 关键字把多条语句操作的共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
如下所示:
public class SellTicket implements Runnable {
//定义一个成员变量表示有100张票
private int tickets=100;
private Object obj=new Object();
public void run(){
while (true){
//这里放的锁要是同一把锁才可以
synchronized(obj){
if(tickets>0){
try {
//通过sleep()方法来等待
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets--+"张票");
}else{
//System.out.println("");
}
}
}
}
}
同步的好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会判断同步上的锁,这是很浪费资源的,无形中会降低程序的运行效率
同步方法
同步方法锁
-
注意同步代码块需要输入一个唯一标志——锁。这个锁必须是所有线程只有一把,有人进了,其他人就必须等待。 因此,为了免除重新创建对象作为锁的损耗,可以使用当前方法作为唯一标志,方法的默认锁对象为
this
。private int tickets = 100; private Object obj = new Object(); private int x = 0; @Override public void run() { while (true) { if (x % 2 == 0) { // synchronized (obj) { synchronized (this) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } } } else { sellTicket(); } x++; } } private synchronized void sellTicket() { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } } }
同步静态锁
-
当定义的同步方法是静态方法时,此时的锁对象为
.class
如下所示:public class SynchronizedStatic { public static synchronized void m1(){ try { System.out.println(Thread.currentThread().getName()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } public static synchronized void m2(){ try { System.out.println(Thread.currentThread().getName()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }
直接码角度分析 synchronize
我们可以通过反编译:javap -v -p *.class > 类.txt
将反编译代码进行输出到txt中
synchronized有三种应用方式
- 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁。
- 作用于代码块,对括号里配置的对象加锁
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
①、synchronized同步代码块
- 实现使用的是
monitorenter
和monitorexit
指令。一般情况是下,一个monitorenter 两个 monitorexit 。因为需要释放一个异常锁,一个正常锁。
- 一定是一个enter和两个exit吗?
(不一定,如果方法中直接抛出了异常处理,那么就是一个monitorenter和一个monitorexit)
②、synchronized普通同步方法
- 调用指令将会检查方法的
ACC_SYNCHRONIZED
访问标志是否被设置 - 如果设置了,执行线程会将先持有monitor,然后再执行方法,最后再方法完成时释放minotor(无论是正常完成还是非正常完成)。即即执行线程不会在执行代码时显示持有monitor,如下图所示
③、synchronized静态同步方法
- ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁
生产者和消费者模式概述
所谓生产消费者问题,实际上主要是包含了两类线程:
- 一类是生产者线程用于生产数据
- 一类是消费者线程用于消费数据
为了耦合生产者和消费者的关系,通常会采用共享的数据区域,就像一个仓库:
- 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
- 消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为
多线程加锁时徐亚注意的事项
线程四句口诀 掌握
-
在高内聚低耦合的前提下,线程 - >操作 - >资源类
假如有一个空调,三个人去操作这个空调,高内聚低耦合是指空调有制热制冷的效果,它会把这两个抽取成一个方法,对外以接口的形式去暴露,提供给操作空调的人或线程使用
-
判断|操作|唤醒 [ 生产消费中 ]
-
多线程交互中,必须要防止多线程的虚假唤醒,也即(判断使用while,不能使用if)
(后面会提到) -
标志位
使用Sychronized实现(隐式锁)
为了体现生产和消费过程总的等待和唤醒,Object类提供了等待和唤醒方法(隐式锁)。
viod wait( ):
导致当前线程等待,直到另一个线程调用该对象的notify()方法和notifyAll()方法void notify( ):
唤醒正在等待对象监视器的单个线程void notifyAll( ):
唤醒正在等待对象监视器的所有线程
(注意:wait、notify、notifyAll方法必须要在同步块或同步方法里且成对出现使用)
例子: 现在4个线程,可以操作初始值为0的一个变量,实现2个线程对该变量加1,
2个线程对该变量减1,交替执行,来10轮,变量的初始值为0。
/*
2.思想:
1.在高内聚低耦合的前提下,线程->操作->资源类
2.判断操作唤醒[生产消费中]
3.多线程交互中,必须要放置多线程的虚假唤醒,也即(判断使用while,不能使用if)
* */
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
AirCondition airCondition=new AirCondition();
new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.increment();},"线程A").start();
new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.decrement();},"线程B").start();
new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.increment();},"线程C").start();
new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.decrement();},"线程D").start();
}
}
class AirCondition{
private int number=0;
public synchronized void increment(){
//1。判断当前状态
/* if(number!=0){*/
while(number!=0){
try {
//为什么不用if?解释如下
//第一次A进来了,在number++后(number=1) C抢到执行权,进入wait状态
//这个时候,A抢到cpu执行权,也进入wait状态,此时,B线程进行了一次消费
//唤醒了线程,这个时候A抢到CPU执行权,不需要做判断,number++(1),唤醒线程
//C也抢到CPU执行权,不需要做判断,number++(2)
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2.干活
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
//3.唤醒
this.notifyAll();
}
public synchronized void decrement(){
/*if (number==0){*/
while (number==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
this.notifyAll();
}
}
使用ReentrantLock实现 (显示锁)
Lock 接口
Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁, 当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
-
通常使用 Lock 来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...; lock.lock(); try{ //处理任务 }catch(Exception ex){ }finally { lock.unlock(); //释放锁 }
该接口存在下面这些接口函数:
public interface Lock
{void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
newCondition
- 关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。
- 用
notify()
通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:await()
会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重
新获得锁并继续执行。signal()
用于唤醒一个等待的线程。
注意: 在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
ReentrantLock
-
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念将在后面讲述。
-
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更
多的方法。下面通过一些实例看具体看一下如何使用。- ①.
ReentrantLock( )
:创建一个ReentrantLock的实例 - ②.
void lock( )
:获得锁 - ③.
void unlock( )
:释放锁
- ①.
例子:生产者与消费者
/*
* 使用Lock代替Synchronized来实现新版的生产者和消费者模式 !
* */
@SuppressWarnings("all")
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
AirCondition airCondition=new AirCondition();
new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.decrement();},"线程A").start();
new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.increment();},"线程B").start();
new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.decrement();},"线程C").start();
new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.increment();},"线程D").start();
}
}
class AirCondition{
private int number=0;
//定义Lock锁对象
final Lock lock=new ReentrantLock();
final Condition condition = lock.newCondition();
//生产者,如果number=0就 number++
public void increment(){
lock.lock();
try {
//1.判断
while(number!=0){
try {
condition.await();//this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2.干活
number++;
System.out.println(Thread.currentThread().getName()+":\t"+number);
//3.唤醒
condition.signalAll();//this.notifyAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
//消费者,如果number=1,就 number--
public void decrement(){
lock.lock();
try {
//1.判断
while(number==0){
try {
condition.await();//this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2.干活
number--;
System.out.println(Thread.currentThread().getName()+":\t"+number);
//3.唤醒
condition.signalAll();//this.notifyAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
ReadWriteLock 接口
ReadWriteLock 也是一个接口,在它里面只定义了两个方法:
//返回读锁
Lock readLock();
//返回写锁
Lock writeLock();
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。
ReentrantReadWriteLock
ReentrantReadWriteLock
里面提供了很多丰富的方法,不过最主要的有两个 方法``readLock()和
writeLock()`用来获取读锁和写锁。
-
线程进入读锁的前提条件:
- 没有其他线程的写锁,
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
- 也就是说可以多个线程进入读锁
-
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
-
读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁
线程间的定制化通信:顺序执行线程
案例介绍
- 多个线程之间按顺序调用,实现A->B->C
- 三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
…来10轮
由于需要考虑到线程顺序,因此,这里在获得锁和释放锁时,我们应当手动指定唤醒的对象
/*
多个线程之间按顺序调用,实现A->B->C
三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
....来10轮
* */
public class ThreadOrderAccess {
public static void main(String[] args) {
ShareResource shareResource=new ShareResource();
new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print5(); },"线程A").start();
new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print10(); },"线程B").start();
new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print15(); },"线程C").start();
}
}
class ShareResource{
//设置一个标识,如果是number=1,线程A执行...
private int number=1;
Lock lock=new ReentrantLock();
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
Condition condition3=lock.newCondition();
public void print5(){
lock.lock();
try {
//1.判断
while(number!=1){
condition1.await();
}
//2.干活
for (int i = 1; i <=5; i++) {
System.out.println(Thread.currentThread().getName()+":\t"+i);
}
//3.唤醒
number=2;
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10(){
lock.lock();
try {
//1.判断
while(number!=2){
condition2.await();
}
//2.干活
for (int i = 1; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+":\t"+i);
}
//3.唤醒
number=3;
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15(){
lock.lock();
try {
//1.判断
while(number!=3){
condition3.await();
}
//2.干活
for (int i = 1; i <=15; i++) {
System.out.println(Thread.currentThread().getName()+":\t"+i);
}
//3.唤醒
number=1;
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
虚假唤醒
虚假唤醒问题的产生
例子:要求用代码实现下面需求:
- 一个卖面的面馆,有一个做面的厨师和一个吃面的食客,需要保证,厨师做一碗面,食客吃一碗面,不能一次性多做几碗面,更不能没有面的时候吃面;按照上述操作,进行十轮做面吃面的操作。
两个线程的情况
两个线程不会出现虚假唤醒问题,四个或多个线程才会出现
class Noodles{
//面的数量
private int num = 0;
//做面方法
public synchronized void makeNoodles() throws InterruptedException {
//如果面的数量不为0,则等待食客吃完面再做面
if(num != 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"做好了一份面,当前有"+num+"份面");
//面做好后,唤醒食客来吃
this.notifyAll();
}
//吃面方法
public synchronized void eatNoodles() throws InterruptedException {
//如果面的数量为0,则等待厨师做完面再吃面
if(num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"吃了一份面,当前有"+num+"份面");
//吃完则唤醒厨师来做面
this.notifyAll();
}
}
public class Test {
public static void main(String[] args) {
Noodles noodles = new Noodles();
new Thread(new Runnable(){
@Override
public void run() {
try {
for (int i = 0; i < 10 ; i++) {
noodles.makeNoodles();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"厨师A").start();
new Thread(new Runnable(){
@Override
public void run() {
try {
for (int i = 0; i < 10 ; i++) {
noodles.eatNoodles();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"食客甲").start();
}
}
多个线程的情况
如果有两个厨师,两个食客,都进行10次循环呢?(出现线程虚假唤醒问题)
Noodles类的代码不用动,在主类中多创建两个线程即可,主类代码如下:
public class Test {
public static void main(String[] args) {
Noodles noodles = new Noodles();
new Thread(new Runnable(){
@Override
public void run() {
try {
for (int i = 0; i < 10 ; i++) {
noodles.makeNoodles();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"厨师A").start();
new Thread(new Runnable(){
@Override
public void run() {
try {
for (int i = 0; i < 10 ; i++) {
noodles.makeNoodles();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"厨师B").start();
new Thread(new Runnable(){
@Override
public void run() {
try {
for (int i = 0; i < 10 ; i++) {
noodles.eatNoodles();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"食客甲").start();
new Thread(new Runnable(){
@Override
public void run() {
try {
for (int i = 0; i < 10 ; i++) {
noodles.eatNoodles();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"食客乙").start();
}
}
出现原因
从上面结果可以看到,我们本来需要不能一次性多做几碗面,更不能没有面的时候吃面,但是上面结果却出现了这种情况。
我们一步一步对吃面情况进行分析,如下:
- ①. 初始状态
- ②. 厨师A得到操作权,发现面的数量为0,可以做面,面的份数+1,然后唤醒所有线程;
- ③. 厨师B得到操作权,发现面的数量为1,不可以做面,执行wait操作;
- ④. 厨师A得到操作权,发现面的数量为1,不可以做面,执行wait操作;
- ⑤. 食客甲得到操作权,发现面的数量为1,可以吃面,吃完面后面的数量-1,并唤醒所有线程;
- ⑥. 此时厨师A得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程
- ⑦. 此时厨师B得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程
综上:此时就会生产处两碗面
解决方案
循环判断,而不是判断一次就走人
if(num != 0){
this.wait();
}
改为
while(num != 0){
this.wait();
}
if(num == 0){
this.wait();
}
改为
while(num == 0){
this.wait();
}