java锁机制、互斥机制和同步

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执行如上面的牛奶案例
  • 通信:确保不会无休止的等待
    • wait():暂停自己,释放锁,进入等待
    • notify()、notifyAll():唤醒调用相同对象的wait()的一个/所有线程

注意:sleep()不会释放锁,不能用作通信。

  • 在这里插入图片描述
  • 小结:
    • 同步实现的是多个线程间一次节奏的原子性(如一次生产必然对应一次消费)。而非单个线程操作的原子性
    • 互斥:定义一次完整的节奏。如生产- 消费必须一一对应
    • 通信:确保两个线程避免死等,被wait()的线程必须释放资源,释放锁

猜你喜欢

转载自blog.csdn.net/Phoebe4/article/details/111699331