Java 线程安全及线程同步synchronized

目录

 

Java 线程安全及线程同步synchronized

同步处理的引入

问题

出错的解决方案

写法一:同步方法

写法二:同步代码块

写法3:同步静态方法

重点

银行存取钱操作

小结synchronized

synchronized底层语义原理

多线程之间的数据通讯方法

生产者消费者模式

为什么要使用生产者/消费者模式

生产者/消费者模型优点

生产者/消费者模式的作用

练习题

售票模拟

过山洞问题模拟

龟兔赛跑模拟

Lock的使用

Condition接口


Java 线程安全及线程同步synchronized

多线程的执行顺序不可重现,但是必须要求执行结果必须可以重现!

线程的共享数据操作不完整性就一定会出现数据被破坏,而导致结果无法预知的问题---线程的安全问题。

同步处理的引入

在java语言中存在两种内建的synchronized语法:synchronized代码块和synchronized方法( 静态方法和非静态方法)可以解决线程安全问题

首先synchronized将并行改为串行,当然会影响程序的执行效率,执行速度会受到影响。其次synchronized操作线程的堵塞,也就是由操作系统控制CPU的内核进行上下文的切换,这个切换本身也是耗时的。所以使用synchronized关键字会降低程序的运行效率。

问题

线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点:

  1. 是存在共享数据(也称临界资源)
  2. 是存在多条线程共同操作共享数据

出错的解决方案

当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。

  • 在Java中每个对象都有一个对象锁,其实就是对象头上的一个标记值而已

  • 同步处理的目标实际上就是实现线程排队执行的目的

写法一:同步方法

针对临界资源的修改方法上添加同步约束---synchronized 。

//临界资源---被多个线程所共享操作的对象
public class OperNum {
	private int target;//操作的数据
	//针对操作数据的方法
	public synchronized void add() {//synchronized方法的同步锁为当前对象
		target++;
		System.out.println(Thread.currentThread().getName()+"add...."+target);
	}
	public synchronized void sub() {
		target--;
		System.out.println(Thread.currentThread().getName()+"sub...."+target);
	}
}

原理:当一个线程在执行add方法时,其它线程不能执行add或者sub方法【同步方法都不能执行,因为是synchronized关键字会引入一个互斥锁,只有拥有锁的线程才能执行同步方法,其它线程只能阻塞等待】,synchronized属于重入锁,即当前线程可以执行其它的synchronized方法,但是其它线程不能执行当前对象中的synchronized方法,可以执行没有synchronized约束的方法 。

写法二:同步代码块

锁实际上是人为指定的,推荐使用 synchronized(锁){}

package xiancheng;

public class D21 extends Thread {
	private static int target = 0; // 操作目标,临界资源
	private boolean flag = true; // 不是临界资源
	// 定义一个互斥锁
	private static Object lock = new Object();
	public D21(boolean flag) {
		this.flag = flag;
	}
	public void run() {
		for (int i = 0; i < 50; i++) {
			//synchronized (lock) 可以保证{}中的代码执行具备原子性
			synchronized (lock) {
				if (flag) target++;
				else target--;
				System.out.println(Thread.currentThread().getName() + (flag ? "add" : "sub") + "...." + target);
			}
		}
	}
	public static void main(String[] args) {
		for(int i=0;i<4;i++) {
			new D21(i%2==0).start();
		}
	}
}

写法3:同步静态方法

以当前类对象充当锁,所有静态方法互斥

  • 和非静态方法不互斥,因为非静态同步方法的锁是当前对象,不是类锁

package xiancheng;

//临界资源
public class D22 extends Thread {
	private static int target = 10;// 操作的数据
	// 针对操作数据的方法

	public synchronized static void add() {
		for (int i = 0; i < 3; i++) {
			target++;
			System.out.println(Thread.currentThread().getName() + "add...." + target);
		}
	}

	public synchronized static void sub() {
		for (int i = 0; i < 3; i++) {
			target--;
			System.out.println(Thread.currentThread().getName() + "sub...." + target);
		}
	}

	public void run() {
		this.add();//普通方法的话两个对象锁不住
		this.sub();

	}

	public static void main(String[] args) {
		Thread a = new D22();//静态类锁两个对象开启的线程也可以锁
		Thread b = new D22();
		a.start();
		b.start();

	}
}

在方法上添加synchronized关键字后就可以保证在一个时刻上只有一个线程在调用某个方法【锁只能有一个】,不会出现并发的情形,达到排队执行的效果。

  • 在Java中synchronized可保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),这点也是很重要的。

  • 常见的4种Java线程锁:原子量AtomicInteger、信号量Semaphone、同步处理synchronized和重入锁ReentrantLock

  • jdk6之前是重量级锁,JDK6开始优化为锁的状态总共有四种,无锁状态(没有synchronized)、偏向锁、轻量级锁和重量级锁。锁状态的改变是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁, 在重度竞争的情况下,会升级到重量级锁。 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

重点

  • 两个线程分别调用同一个对象中的aaa方法【方法中有sleep】和bbb方法,一个同步一个不同步,会不会有等待问题

    • 临界资源对象中的同步方法和非同步方法不互斥,所以可以同时运行

  • 两个方法都有synchronized则有等待

    • 临界资源对象中的同步方法是互斥的,所以只能获取锁的线程运行,另外一个处于阻塞等待

  • 一个方法静态,一个方法非静态

    • static方法使用的是类锁,而非静态方法使用的是对象锁

  • 两个方法都是静态,因为类只有一个

    • 如果两个方法都是静态的,则使用的都是类锁,所以会有等待

  • 方法中采用wait

    • wait方法会有释放锁,而sleep不释放锁

银行存取钱操作

package xiancheng;

public class D14 {
	private int money;

	public int getMoney() {
		return money;
	}

	public void setMoney(int money) {
		this.money = money;
	}

	public D14(int money) {
		this.money = money;
	}

	public static void main(String[] args) {
		D14 a = new D14(0);
		Thread t1 = new Thread(new 存钱线程(a,10000));
		Thread t2 = new Thread(new 取钱线程(a,100));
		Thread t3 = new Thread(new 取钱线程(a,900));
		t1.start();
		t3.start();
		t2.start();
	}

}
class 存钱线程 implements Runnable{
	private D14 account;
	private int mon;
	public 存钱线程(D14 account,int mon) {
		this.account = account;
		this.mon = mon;
	}

	public void run() {
		synchronized(account) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			account.setMoney(account.getMoney()+mon);
			System.out.println("存钱成功 余额是:"+account.getMoney());
		}
		
	}
	
}
class 取钱线程 implements Runnable{
private D14 account;
private int mon;

	public 取钱线程(D14 account,int mon) {
	this.account = account;
	this.mon = mon;
}

	public void run() {
		synchronized (account) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if(account.getMoney()>mon) {
				account.setMoney(account.getMoney()-mon);
				System.out.println("取钱成功 余额是:"+account.getMoney());
			}else {
				System.out.println("余额不足!");
			}
		}
		
	}
}
/*
存钱成功 余额是:10000
取钱成功 余额是:9900
取钱成功 余额是:9000
*/

小结synchronized

synchronized的三种应用:对象锁、类锁和同步块

注意synchronized不能修饰构造器、变量、内部类

同步实例方法

修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

方法上添加synchronized就叫做同步方法,例如public synchronized void draw(double amount){}

注意:在一个类对象中所有的同步方法是互斥的

  • 只要有一个线程进行了当前类对象(只能一个对象)的同步方法,则不允许其它线程在进入当前这个对象的任何同步方法,但是允许进入非同步方法

  • 同样当前线程则可以进入当前类对象的其它同步方法,也允许进入非同步方法,当线程进入同步方法,则获取同步锁,离开同步方法则释放同步锁

  • 这个锁就是当前类对象

这种方法不是最佳选择,因为这里的同步处理颗粒度太大了(所有当前对象中的同步处理方法都是互斥的),会影响并发性!

线程安全的类:是通过使用同步方法的类,同步监视器是this

  • 该类的对象可以被多个线程安全地访问

  • 每个线程调用该对象的任意方法之后都将得到正确结果

  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

StringBuilder和StringBuffer

  • StringBuilder是线程不安全的类,数据不安全,但是并发执行效率高,一般用于定义临时变量

  • StringBuffer是线程安全的类,数据安全,但是并发执行效率差,一般用于定义属性


同步静态方法

修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

同步监视器是当前类对象Class c=Date.class

如果在方法上去掉static,则不能实现锁定效果,因为是对象锁,而在线程中创建了10个对象,各个对象没有任何关系,所以不能达到锁定的效果,但是类锁的效果仍旧有效!

同步方法块

上面的两种方式比较死板,普通方法同步是以当前对象作为锁,静态方法同步是以当前类对象作为锁

所以引入更为灵活的方式:同步块,将锁对象作为参数进行传递

synchronized(account){}同步监视器可以阻止多个线程对同一个共享资源的并发访问,任何时刻只有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放堆该同步监视器的锁定。

注意:在使用锁的过程中注意wait的使用问题,否则可能会导致java.lang.IllegalMonitorStateException

synchronized底层语义原理

对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。而synchronized方法则会被翻译成普通的方法调用和返回指令,在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象

使用synchronized加锁的字节码会出现monitorenter和monitorexit两个指令,可以理解为代码块执行前的加锁和退出同步时的解锁

  • 执行monitorenter指令时,线程会为锁对象关联一个ObjectMonitor对象。

  • 线程遇到synchronized同步时,先会进入ObjectMonitor对象的EntryList队列中,然后尝试把ObjectMonitor对象的owner变量设置为当前线程,同时ObjectMonitor对象的monitor中的计数器count加1,即获得对象锁。否则通过尝试自旋一定次数加锁,失败则进入ObjectMonitor对象的cxq队列阻塞等待

  • synchronized是可重入,非公平锁,因为entryList的线程会先自旋尝试加锁,而不是加入cxq排队等待,不公平

何时需要同步

在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。

对于非静态字段中可更改的数据,通常使用非静态方法访问。

对于静态字段中可更改的数据,通常使用静态方法访问。

释放同步锁

  • 同步代码执行结束

  • 同步方法中遇到break或return终止代码

线程同步方法是通过锁(监视者)来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的同步方法(可以访问静态同步方法)。

对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

对于同步,要时刻清醒在哪个对象上同步

  • 同步方法--当前对象

  • 静态同步方法--当前类

  • 同步代码块--任意指定

编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对需要具有原子操作的步骤做出分析,并保证原子操作期间别的线程无法访问竞争资源(加锁处理)

  • StringBuilder和StringBuffer

  • JDK6+开始对synchronized进行优化,引入了偏向锁、轻量级锁和重量级锁

当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞

  • wait方法和sleep方法

死锁是线程间相互等待锁造成的,在实际中发生的概率非常的小。但是一旦程序发生死锁,程序将死掉。

多线程之间的数据通讯方法

生产者消费者模式

生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括生产者、消费者、仓库和产品。他们之间的关系如下:

  • 生产者仅仅在仓储未满时候生产,仓满则停止生产。

  • 消费者仅仅在仓储有产品时候才能消费,仓空则等待。

  • 当消费者发现仓库没产品可消费时候会通知生产者生产。

  • 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

为什么要使用生产者/消费者模式

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。

生产者/消费者模型优点

1、解耦。因为多了一个缓冲区,所以生产者和消费者并不直接相互调用,这一点很容易想到,这样生产者和消费者的代码发生变化,都不会对对方产生影响,这样其实就把生产者和消费者之间的强耦合解开,变为了生产者和缓冲区/消费者和缓冲区之间的弱耦合

2、通过平衡生产者和消费者的处理能力来提高整体处理数据的速度,这是生产者/消费者模型最重要的一个优点。如果消费者直接从生产者这里拿数据,如果生产者生产的速度很慢,但消费者消费的速度很快,那消费者就得占用CPU的时间片白白等在那边。有了生产者/消费者模型,生产者和消费者就是两个独立的并发体,生产者把生产出来的数据往缓冲区一丢就好了,不必管消费者;消费者也是,从缓冲区去拿数据就好了,也不必管生产者,缓冲区满了就不生产,缓冲区空了就不消费,使生产者/消费者的处理能力达到一个动态的平衡

生产者/消费者模式的作用

  • 支持并发

  • 解耦

  • 支持忙闲不均

调用wait/notify之类的方法要求必须在当前线程对象内部,例如synchronized方法中

package xiancheng;

import java.util.Date;

//临界资源
public class Basket1 {//仓库类,产品就是仓库的属性date
	private volatile Object data;
	//生产者向仓库中存放数据
	public synchronized void product(Object data) {
		//如果仓库中有数据,则生产者进入阻塞等待,直到其它线程唤醒
		while(this.data!=null)//条件决定仓库不空了就阻塞所以仓库容量为1
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		//如果没有数据则进行生产操作
		this.data=data;
		System.out.println(Thread.currentThread().getName()+"生产了一个日期"+this.data);
		this.notify();//唤醒在当前对象上处于wait的所有线程
	}
	//消费者从仓库中消费数据
	public synchronized void consume() {
		//如果仓库中没有数据,则消费者进入阻塞等待,直到其它线程唤醒
		while(this.data==null)
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		//如果有数据data!=null,则执行消费操作
		System.out.println(Thread.currentThread().getName()+"消费了一个数据"+this.data);
		this.data=null;
		this.notify();  //唤醒在当前对象上处于wait的所有线程
	}
	public static void main(String[] args) {
		Basket1 a = new Basket1();
		Thread T1 = new Producer(a);
		Thread T2 = new Consumer(a);
		T1.start();
		T2.start();
	}
}
class Producer extends Thread {//生产线程
	private Basket1 basket;
	//通过构造器传入对应的basket对象
	public Producer(Basket1 basket) {
		this.basket=basket;
	}
	@Override
	public void run() {
		//生产20次日期对象
		for(int i=0;i<20;i++) {
			Object data=new Date();  //生产者生产的具体产品
			basket.product(data);
		}
	}
}
 class Consumer extends Thread {//消费线程
	private Basket1 basket;

	public Consumer(Basket1 basket) {
		this.basket = basket;
	}

	@Override
	public void run() {
		// 消费20次日期对象
		for (int i = 0; i < 20; i++) {
			basket.consume();
		}
	}
}

练习题

售票模拟

package xiancheng;

public class D16 implements Runnable {// 三个线程同时出售二十张票
	private volatile int account = 1;
	public void run() {
		while (true) {
			synchronized (this) {
				if (account > 2000) {//数字越大结果更能反应线程执行规律
					System.out.println("票已经卖完了!");
					break;
				}
				if (account < 2001 && account > 0) {
					System.out.println(Thread.currentThread().getName() + "售出第" + account + "张票");
					account++;
				}
			}

		}
	}

	public static void main(String[] args) {
		D16 A = new D16();
		Thread T1 = new Thread(A, "一号窗口");
		Thread T2 = new Thread(A, "二号窗口");
		Thread T3 = new Thread(A, "三号窗口");
		T1.start();
		T2.start();
		T3.start();
	}

}

过山洞问题模拟

package xiancheng;

import java.util.Random;

public class D17 {
	public static void main(String[] args) {
		String[] arr = new String[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J" };
		Thread[] ts = new Thread[arr.length];
		Random r = new Random();
		山洞 shan = new 山洞();
		for (int i = 0; i < arr.length; i++) {
			String name = null;
			while (name == null) {
				int pos = r.nextInt(arr.length);
				String temp = arr[pos];
				if(temp!=null) {
					name = temp;
					arr[pos] = null;
				}
			}
			ts[i]=new Thread(()-> {
				shan.过山洞();
			},name);
			ts[i].start();
		}
		for(Thread temp:ts) {
			if(temp!=null) {
				try {
					temp.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		shan.show();

	}
} 

class 山洞 {// 临界资源,多个线程过一个山洞

	private StringBuffer logs = new StringBuffer();// 日志记录
	private int num = 0;// 到达山洞口的序号 需要过山洞的人
	private static final Object Lock = new Object();// 锁
    private int counter = 0;//穿过山洞口的序号 统及穿过山洞的人
	public void 过山洞() {
		logs.append("第"+(++num)+"人"+Thread.currentThread().getName()+"到达山洞口"+"\n");
		synchronized(Lock) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			logs.append("第"+(++counter)+"人"+Thread.currentThread().getName()+"穿过山洞"+"\n");
			
		}

	}
	public void show() {
		System.out.println(logs);
	}

}

龟兔赛跑模拟

package xiancheng;

import xiancheng.动物.结束接口;//内部接口导到外边

public class D18 {// 龟兔赛跑
	public static void main(String[] args) {
		兔子 tuzi = new 兔子();
		乌龟 wugui = new 乌龟();
		tuzi.setEnd(new 结束接口的实现类(wugui));
		wugui.setEnd(new 结束接口的实现类(tuzi));
		tuzi.start();
		wugui.start();
	}

}

abstract class 动物 extends Thread {
	public static final int 总长度 = 100;
	protected int 剩余长度 = 总长度;
	protected 结束接口 end;

	static interface 结束接口 {
		public void 结束();
	}

	public void setEnd(结束接口 end) {
		this.end = end;
	}

	public void run() {
		while (剩余长度 > 0) {
			跑步();
		}
	}

	protected abstract void 跑步();

}

class 兔子 extends 动物 {
	// 规则兔子每0.1秒5米的速度, 每跑20m休息一秒
	public void 跑步() {
		int dis = 5;
		剩余长度 -= dis;
		System.out.println("兔子跑了" + dis + "米,距离终点还有" + 剩余长度 + "米");
		if (剩余长度 <= 0) {
			剩余长度 = 0;
			System.out.println("兔子获得了胜利!");
			if (end != null) {
				end.结束();
			}
		}
		try {
			sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if (剩余长度 % 20 == 0) {
			try {
				sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

	}
}

class 乌龟 extends 动物 {
	// 乌龟每0.1秒跑2m不休息
	public void 跑步() {
		int dis = 2;
		剩余长度 -= dis;
		System.out.println("乌龟跑了" + dis + "米,距离终点还有" + 剩余长度 + "米");
		if (剩余长度 <= 0) {
			剩余长度 = 0;
			System.out.println("乌龟获得了胜利!");
			if (end != null) {
				end.结束();
			}
		}
		try {
			sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}
}

class 结束接口的实现类 implements 结束接口 {
	private Thread otherThread;

	public 结束接口的实现类(Thread other) {
		this.otherThread = other;

	}

	public void 结束() {
		if (otherThread != null) {
			otherThread.stop();
		}

	}

}

Lock的使用

Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口,可以通过显式定义同步锁对象来实现同步,能够提供比synchronized更广泛的锁定操作,并支持多个相关的Condition对象。

  • void lock();尝试获取锁,获取成功则返回,否则阻塞当前线程

void lockInterruptibly() throws InterruptedException;尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常

boolean tryLock();尝试获取锁,获取锁成功则返回true,否则返回false

boolean tryLock(long time, TimeUnit unit)尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常

  • void unlock();释放锁

  • Condition newCondition();返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量

Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。

使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。

private final ReentrantLock lock=new ReentrantLock();

在具体方法中lock.lock() try{}finally{lock.unlock}

Lock lock=new ReentrantLock();//构建锁对象
try{
	lock.lock();//申请锁,如果可以获取锁则立即返回,如果锁已经被占用则阻塞等待
	System.out.println(lock);//执行处理逻辑
} finally{
	lock.unlock();//释放锁,其它线程可以获取锁
}

Condition接口

Condition是java.util.concurrent.locks包下提供的一个接口。可以翻译成 条件对象,其作用是线程先等待,当外部满足某一条件时,在通过条件对象唤醒等待的线程。

void await() throws InterruptedException;让线程进入等待,如果其他线程调用同一Condition对象的notify/notifyAll,那么等待的线程可能被唤醒。释放掉锁。

void signal();唤醒等待的线程。

void signalAll();唤醒所有线程。

package xiancheng;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;//Lock的实现类

public class D20 {//左右手交替执行
	int flag = 0;
	private static final Lock lock = new ReentrantLock();
	private static final Condition conleft = lock.newCondition();
	private static final Condition conright = lock.newCondition();

	public void leftprint() {
		for (int i = 0; i < 50; i++) {
			lock.lock();
			while (flag == 1) {
				try {
					conleft.await();// 休眠状态并释放锁 类似wait
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName()+"左手一个慢动作!");
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			flag = 1;
			conright.signal();// 类似notify唤醒线程
		}
	}

	public void rightprint() {
		for (int i = 0; i < 50; i++) {
			lock.lock();
			while (flag == 2) {
				try {
					conright.await();// 休眠状态并释放锁 类似wait
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName()+"右手一个慢动作!");
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			flag = 2;
			conleft.signal();// 类似notify唤醒线程
		}
	}

	public static void main(String[] args) { 
		D20 a = new D20();
		new Thread(() -> {
			a.rightprint();
		}).start();
		new Thread(() -> {
			a.leftprint();
		}).start();

 	}

}

写2个线程,其中一个打印1-52,另一个打印a-z,打印顺序应该是12a34b56c……5152

  • 线程1执行2次数后,线程2执行1次输出

package xiancheng;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class D23 {//写2个线程,其中一个打印1-52,另一个打印a-z,打印顺序应该是12a34b56c……5152

// 线程1执行2次数后,线程2执行1次输出

	private static final Lock lock = new ReentrantLock();
	private static final Condition conNum = lock.newCondition();
	private static final Condition conChar = lock.newCondition();
	private boolean flag = false;

	public void printNum() {
		try {
			lock.lock();
			for (int i = 1; i <= 52; i++) {
				while (flag)
					try {
						conNum.await();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				System.out.print(i);
				if (i % 2 == 0) {
					flag = !flag;
					conChar.signal();
				}
			}
		} finally {
			lock.unlock();
		}
	}

	public void printChar() {
		try {
			lock.lock();
			for (int i = 'a'; i <= 'z'; i++) {
				while (!flag)
					try {
						conChar.await();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				System.out.print((char)i);
					flag = !flag;
					conNum.signal();
			}
		} finally {
			lock.unlock();
		}
	}
	public static void main(String[] args) {
		D23 tt=new D23();
		new Thread(()->{
			tt.printNum();
		}).start();
		new Thread(()->{
			tt.printChar();
		}).start();
	}
}
/*
 * 12a34b56c78d910e1112f1314g1516h1718i1920j2122k2324l2526m2728n2930o3132p3334q3536r3738s3940t4142u4344v4546w4748x4950y5152z
 */

wait() 和 notify() 方法的调用必须具有内置锁 synchronized(this) 的代码块内才能调用,否则就会报该错误。
既然用了显式锁 Lock 就不要用 wait 和 notify 了,它们是两套加锁机制,不能混着用的。应该用 lock 创建一个 Condition ,然后在该 Condition 上 await()。


版权声明:本文为yuan_dai_ma原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。