大飞老师带你再看Java线程(六)

名称解释

共享资源:在多线程并发环境下,多个线程可以操作同一个数据,这数据便是共享资源

临界资源:一次仅允许一个线程使用的资源称为临界资源

竞争资源:多线程并发环境下,为保证线程安全无误执行,对操作数据有序的争夺.这类数据称之为竞争资源.

线程同步:同步就是协同步调,按预定的先后次序运行. 线程同步, 当一个线程在对数据进行操作时,其他线程都不可以对这个数据进行操作,直到该线程完成操作, 其他线程才能对该数据进行操作.

线程安全:在多线程并发环境下, 线程能通过同步机制保证各个线程都可以正常且正确的执行,不出现数据污染等意外情况

临界区:指程序中的一个代码段,在这段代码中,有且只有一个线程可对其进行访问。java中可以synchronized关键字标识一个临界区.

对象锁: java每一个对象都拥有一把锁(对象锁),约定线程能进入临界区前提, 必须持有事先设置好的对象锁.

类锁: Class对象锁。

获取锁: 当前线程为进入临界区而争夺到的锁对象.

synchronized 同步方法使用

先看一个案例:2个线程同时操作同一个成员变量

public class RedPacket  implements  Runnable{
    private int count = 5; //默认5个
    public void run() {
        //红包数大于0, 可以抢
        while (count > 0){
            //放大效果
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            grabRed();
        }
    }
    public  void grabRed(){
        if(count > 0){
            System.out.println(Thread.currentThread().getName() + " 抢了编码为:" + count +"红包");
            count--;
        }
    }
}
public class App {
    public static void main(String[] args) {
        RedPacket red = new RedPacket();
        //2个线程抢红包
        Thread t1 = new Thread(red, "t1");
        Thread t2 = new Thread(red, "t2");
        t1.start();
        t2.start();
    }
}

打印结果:

t1 抢了编码为:5红包
t1 抢了编码为:3红包
t2 抢了编码为:3红包
t1 抢了编码为:1红包
t2 抢了编码为:1红包

上述打印结果,发现几个问题:
1>3号红包跟1号红包被抢了2次,
2>4号2号红包不见了
这是什么原因呢?其实很简单,JVM调度线程,为线程分配CPU的使用权时,是随机的,带有很强的概率性。所以,当多个线程对同一个资源(红包数量count)读写操作时,线程t1 t2执行的顺序也是随机。这就导致前面讲的线程安全问题。

解决:
使用synchronized关键字修饰grabRed方法即可:

    public synchronized void grabRed(){
        if(count > 0){
            System.out.println(Thread.currentThread().getName() + " 抢了编码为:" + count +"红包");
            count--;
        }
    }

synchronized修饰了grabRed() 方法, 当第一个线程执行grabRed方法时, 马上获取当前对象(RedPacket)对象锁,进而拥有执行grabRed方法执行权。在线程没有执行完grabRed方法,释放对象锁前,所有需要调用grabRed方法线程都必须等待。这可以类比,多个人要蹲坑,一人手快,先进坑了,随手锁门。其他人只能在外面等着,等上个人出来在进。

使用注意

1>synchronized 修饰的方法,表示持有当前操作对象的对象锁
2>synchronized 持有对象锁是重入的
锁重入:简单的讲,是同一个线程可以重复获取同一个对象锁

public class Resource implements Runnable{
    public void run() {

    }
    public synchronized  void method1(){
        System.out.println("调用了方法1.....");
        method2(); //调用方法2
    }

    public synchronized  void method2(){
        System.out.println("调用了方法.....");
    }
}

上面代码, 线程t1可以执行method1方法,也可顺利调用method2方法,这种重复获取Resource对象锁操作,称之为锁重入。这里要注意,线程t1在执行method1方法时,如果线程t2想执行method2时,必须要等待。因为method1, method2执行前提都是相同的Resource对象锁, 而此时对象锁在线程t1手里,所以线程t2必须得等。

3>线程执行方法时,抛出异常,对象锁会自动释放

public class ResouceExt implements Runnable{
    public void run() {
        doSomething();
    }
    public synchronized  void doSomething(){
        System.out.println(Thread.currentThread().getName() + " 开始。。。。");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(1/0);  //模拟异常
    }
}
public class App {
    public static void main(String[] args) throws InterruptedException {
        ResouceExt resouceExt = new ResouceExt();
        Thread t1 = new Thread(resouceExt, "t1");
        Thread t2 = new Thread(resouceExt, "t2");

        t1.start();
        Thread.sleep(1000);
        t2.start();

        System.out.println("----");
    }
}

这个容易理解, 出现1/0 除0异常,doSomething() 方法不做任何处理,向run方法抛,run方法不做处理,那继续向上抛,此时run方法也结束了,那么对应线程就没有持有对象锁的必要了,就可以释放锁了。

4>synchronized持有的锁无法被子类继承

public class Father implements  Runnable {
    public void run() {
    }
    //注意父类方法使用synchronized修饰
    public synchronized  void doSomething(){
        System.out.println("父类dosomething方法.....");
    }
}
//继承父类
public class Son extends Father {
    public void run() {
        doSomething();
    }
    //重写父类方法:注意此时不用synchronized 修饰
    @Override
    public  void doSomething() {
        System.out.println("当前线程:" + Thread.currentThread().getName() + ",进来了....");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前线程:" + Thread.currentThread().getName() + ",出去了....");
    }
}
public class App {
    public static void main(String[] args) throws InterruptedException {
        Son son = new Son();
        Thread t1 = new Thread(son, "t1");
        Thread t2 = new Thread(son, "t2");
        t1.start();
        t2.start();
        System.out.println("----");
    }
}

结果:t1 t2 可以一起进来,一起出去

----
当前线程:t1,进来了....
当前线程:t2,进来了....
当前线程:t1,出去了....
当前线程:t2,出去了....

Son子类的重写父类的doSomething方法,没有使用synchronized修饰,线程t1, 线程t2无需要获取锁便可以进入。说明了synchronized修饰的方法没有继承性。

5>synchronized修饰的方法是静态方法时,线程持有的锁是当前类的字节码对象锁, 也即类锁.

synchronized同步代码块

同步代码操作语法:

synchronized(对象锁){
    //需要同步的代码逻辑块
}

看一个案例:开始4个线程模拟4个售票口卖票

public class Ticket implements Runnable {
    private int count = 10;

    public  void run() {
        while (count > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
            count--;
            //放大效果..
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class App {
    public static void main(String[] args) {
        Ticket ticket  = new Ticket();
        //4个线程售票
        Thread t1 = new Thread(ticket, "t1");
        Thread t2 = new Thread(ticket, "t2");
        Thread t3 = new Thread(ticket, "t3");
        Thread t4 = new Thread(ticket, "t4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

结果: 卖出重号的票 10号 与 5号
t1卖出10号票
t2卖出10号票
t3卖出8号票
t4卖出7号票
t2卖出6号票
t1卖出5号票
t4卖出5号票
t3卖出3号票
t3卖出2号票
t4卖出1号票
t2卖出1号票
分析:
重号的原因是多线程争夺count的时候,出岔子, 比如说, 重号的5号票, t1拿到5号票时,刚输入:”t1卖出5号票”, 还没来得及执行count—操作,那么count=5,就在此时t4线程刚好执行到输出”t4卖出5号票”.所以就出现重号的现象.
解决:
从结果反推,打印跟执行count— 应该同时操作, 不应该割裂开始, 也即, 下面2行代码应该是一体的,同一个时间点, 只允许一个线程操作.

    System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
    count--;

所以可以使用synchronized同步代码块解决

public class Ticket implements Runnable {
    private int count = 10;

    public void run() {
        while (count > 0) {
            synchronized (this) {
                if (count > 0) { //防止为负数的出现
                    System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
                    count--;
                }
            }
            //放大效果..
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

分析:
synchronized (this) 包裹的代码块, 表示在同一个时间点,只允许一个线程执行,其他线程需要等待.this就是线程持有的当前对象的对象锁.

注意:

1> synchronized (this) 表示持有当前对象的对象锁
2>synchronized (对象锁) 多个线程要想达到互斥的效果, 对象锁必须同一个

            synchronized (new Object()) {
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
                    count--;
                }
            }

上面操作无法达到同一时间点,只允许一个线程操作效果. 因为 synchronized (new Object()) 每个线程获取都是不同对象锁,起不到互斥的效果.

synchronized 同步方法与synchronized 同步代码块选用

2种操作其实都一样: 多个线程竞争同一个对象锁,持锁者操作, 无锁者等等.

如果硬要说区别,synchronized 同步代码块控制同步的粒度更小而已.


猜你喜欢

转载自blog.csdn.net/wolfcode_cn/article/details/80900955