java锁机制、互斥机制和同步
(卖票互斥案例和五个线程同步案例)
互斥机制引入
- 之前那个买票案例未用锁机制出现的问题一
(相同票卖多次)
理解过程:每一个线程通过以该实现了runnnable接口的类作为参数依次通过start()方法主动调用run,执行run的过程中,三个线程依次执行run的方法体,因为线程执行的随机性即在每一条语句都可能是某一个线程先抢到CPU的执行权,从而在ticket执行–操作之前,执行了那条输出语句,然后出现输出三个100的情况而之后ticket直接变成07,没有98和99
- 之前那个买票案例未用锁机制出现的问题二(出现负数票)
理解过程:因为在这个条件判断里面,可能当ticket>0的时候比如正好-1时,三个线程已经进入条件语句段了,每个线程执行该段的所有语句之后才会再次判断if里面的条件,就会出现如图所示的情况
判断多线程程序安全问题的标准
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
- 但是因为前面两条是一定满足的,只能从第三条下手,所以可以将操作共享数据的代码段锁起来,卖票那个案例就可以将if语句段锁起来
- 专业一些的解释是:那个代码段即竞争访问共享资源的操作序列,它们执行时不具备原子性
解决策略
- 设置一个标记:即:即标记共享资源D和操作序列S(代码块) 使得S访问D时,具有原子性(一次只有一个线程可以执行)。
标记方式 : synchronized ( D ) { S }
D是临界资源,S是临界区
- 锁机制 :为资源配备自动锁 ,以管控它的使用
- 锁机制注意点:
- 1、每个临界资源都有自动锁,锁开启时允许临界区进入,关闭则拒绝临界区进入
- 2、
临界区一旦进入临界资源就自动闭锁,离开时自动开锁
- 3、所有对象都有锁,基本类型数据没有锁,因为锁机制附着于Object对象基本类型没有锁且不能作为资源
- 4、锁降低了访问资源效率因此只对临界区有效,
即未用synchronized修饰的代码锁机制不起作用
- 锁机制注意点:
class D{ int x; }
class A extends Thread{
D d;
public A(D d1){ d=d1; }
public void run(){
// synchronized(d){
for(int i=1; i<5; i++){
d.x=i; // } //end synchronized(d)
} //end run()
}}
class B extends Thread{
D d;
public B(D d1){ d=d1; }
public void run(){
synchronized(d){
for(int i=1; i<5; i++){
System.out.print(d.x+"="); } //end synchronized(d)
}
}}
public class Ch_7_5_3{
public static void main (String[] args) {
D d=new D();
new A(d).start();
new B(d).start(); } }
例子解析:
- 可以知道上面2个类中有2个synchronized类A和B中各有一个,可以通过分别注释其中一个标志或者两个都注释去看运行结果
- 如果其中任意一个有锁,一个没有,那么没有的那个类仍然可以访问临界资源D这个类,因为此时没有标志
- 如果是两个都有锁,要注意B中才有输出语句,A中没有,A只是修改D.x的值,B中的在堆里面所以D.x都是0,因此,
- 当A先抢到资源则将x改为4,D在循环期间进不去,只能输出四个4,如果B先抢到CPU的执行权,那么执行B的四次输出即四个0
线程的同步机制
-
-
为了实现等待和唤醒需要用到以下几个方法:
- 1、void wait():导致当前线程等待,直到另一个线程调用该对象的notify()或者notifyAll()方法
- 2、void notify()唤醒正在等待对象监视器的单个线程
- 3、void notifyAll():唤醒正在等待对象监视器的所有线程
-
牛奶案例:
-
注意如果没有synchronized标记没有锁会抛出IllegalMonitorStateException异常,即没有监视器。
-
只用了等待没用唤醒会卡住在第一个节奏
package TongbuThread;
public class Box {
//一个成员变量表示第几瓶奶
private int milk;
//定义一个成员变量表示奶箱的状态,否则一直是第五瓶奶
private boolean state=false;
//提供存储牛奶和获取牛奶的操作
public synchronized void put(int milk){
//如果有牛奶应该是等待消费,如果没有就生产牛奶,该类中的putget方法是一次节奏,一次节奏有牛奶就不用生产
if (state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.milk=milk;
System.out.println("送奶工将第"+this.milk+"瓶奶放入奶箱");
//`生产完毕之后修改奶箱的状态`,生产过程就是这个输出语句
state =true;
//生产完毕资源调用结束就唤醒其他等待的线程继续一次节奏的完成
notifyAll();
}
public synchronized void get(){
//如果没有牛奶就等待生产
if(!state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果有牛奶就消费牛奶
System.out.println("用户拿到第"+this.milk+"瓶奶");
state=false;
notifyAll();
}
}
package TongbuThread;
public class Producer implements Runnable{
private Box b;
public Producer(Box b) {
this.b=b;
}
public void run(){
for(int i=1;i<=5;i++){
b.put(i);
//生产者生产5瓶牛奶即确定多少次节奏
}
}
}
package TongbuThread;
public class Customer implements Runnable{
private Box b;
public Customer(Box b){
this.b=b;
}
public void run(){
while(true){
b.get();
}//消费者获取奶箱中的奶
}
}
package TongbuThread;
public class BoxTest {
public static void main(String[] args) {
//创建奶箱对象这是共享数据区域
Box b=new Box();
Producer p=new Producer(b);
Customer c=new Customer(b);
//创建2个线程对象分别将生产者对象和消费者对象作为构造函数传递
Thread t1=new Thread(p);//生产者线程
Thread t2=new Thread(c);//消费者线程
t1.start();
t2.start();
}
}
- 五个线程同步
假定生产饮料经过如下5个步骤:
1、水处理;2、调配杀菌;3、乳化均质;4、灌装封口;5、自动贴标。
假定每一步都由一个线程完成并输出相关信息。设计生产流水线,使得上述步骤有序进行。至少生产5瓶饮料。
即设计需要5个线程参与的同步过程。
package Drinkthread;
public class T extends Thread {
String s;
int x;
int y;
Data d;
public T(String s, int x, int y, Data d) {
this.s=s;
this.x=x;
this.y=y;
this.d=d;
}
public void run(){
int i;
synchronized (d){
for(i=0;i<5;i++){
d.put(s,x,y);
}
}
}
}
class Data{
int flag=1;//相当于那个状态的切换
public void put(String s,int x,int y){
//请补充完整
while(flag!=x)
try {
wait();
}catch (InterruptedException e){
;
}
System.out.println(s);
flag=y;//修改状态
notifyAll();//叫醒其他等待线程
}
}
class A{
public static void main (String[] args) {
Data d=new Data();
T t1=new T("1、水处理",1,2,d);
T t2=new T("2、调配杀菌",2,3,d);
T t3=new T("3、乳化均质",3,4,d);
T t4=new T("4、灌装封口",4,5,d);
T t5=new T("5、自动贴标\n",5,1,d);
t1.start(); t2.start(); t3.start(); t4.start(); t5.start();
}
}
同步机制引入关键
- 如果P发现D(临界资源):如果是等待可能发生是死锁即多个线程循环等待,如果是循环探测太浪费CPU资源
- 因此,实现同步的方案如下:
互斥+线程通信
- 互斥:保证数据安全,访问次序正确
- 共享资源的2个作用:
- 实现线程间的数据交互
- 借助共享资源的状态决定线程执行的次序,例如D空则P执行C等待,D满则P等待,C执行如上面的牛奶案例
- 共享资源的2个作用:
- 通信:确保不会无休止的等待
wait()
:暂停自己,释放锁,进入等待notify()、notifyAll()
:唤醒调用相同对象的wait()的一个/所有线程
注意:sleep()不会释放锁,不能用作通信。
- 小结:
- 同步实现的是
多个线程间一次节奏
的原子性(如一次生产必然对应一次消费)。而非单个线程操作的原子性 - 互斥:定义一次完整的节奏。如生产- 消费必须一一对应
- 通信:确保两个线程避免死等,被
wait()
的线程必须释放资源,释放锁
- 同步实现的是