线程安全(2)

发布于:2022-07-24 ⋅ 阅读:(411) ⋅ 点赞:(0)

线程安全Day-2



一、synchronized锁的粒度问题

上一篇文章演示了线程不安全现象,链接在这儿?
synchronized就可以很好的处理线程不安全问题是???

public class Main1 {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;
    // 定义加减的次数
    static final int COUNT = 10000;
    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    static class Add extends Thread {
        @Override
        public void run() {
            //V2:给整个 for 循环加锁
            synchronized (Main1.class) {
                for (int i = 0; i < COUNT; i++) {
                    r++;
                }
            }
        }
        //V1:只给 r++ 加锁
//        public void run() {
//            for (int i = 0; i < COUNT; i++) {
//                synchronized (Main1.class) {
//                    r++;
//                }
//            }
//
//        }
    }
    static class Sub extends Thread {
        @Override
        public void run() {
            //V2:给整个 for 循环加锁
            synchronized (Main1.class) {
                for (int i = 0; i < COUNT; i++) {
                    r--;
                }
            }
        }
        //V1:只给 r-- 加锁
//        public void run() {
//            for (int i = 0; i < COUNT; i++) {
//                synchronized (Main1.class) {
//                    r--;
//                }
//            }
//        }
    }
    public static void main(String[] args) throws InterruptedException {
        Add add = new Add();
        add.start();

        Sub sub = new Sub();
        sub.start();

        add.join();
        sub.join();
        System.out.println(r);
    }
}

两个线程,都对该类加锁,所以会发生互斥现象,就解决了线程不安全问题。但是又引出一个问题——锁的粒度。上述代码产生线程不安全的语句是 r++ ,r-- 这句代码,我们可以只给 r++ , r-- 进行加锁,也可以对整个 for 循环加锁,这两种方式的加锁粒度不同。

  • 给整个 for 循环加锁 :粗粒度
  • 给 r++,r-- 加锁 :细粒度
  • 本质区别 :临界区代码执行时间的长短

但是也不是说粒度越粗越好,或者越细越好,粒度是需要根据实际的工程情况进行度量的。在初学阶段,可以粗略的理解为:加锁粒度越细,并发可能性越高

二、synchronized解决线程安全问题

上一篇文章中也说到了,线程不安全产生的主要原因有三个:

  • 原子性被破坏
  • 内存可见性导致某些线程读到脏数据
  • 代码重排序导致线程之间关于数据的配合出现了问题

synchronized 就是根据这几点来解决线程安全问题的。

1、原子性

synchronized 可以保证原子性
借鉴上面的代码,给 r++,r-- 加锁就是保证了 r++,r-- 操作的原子性,给整个循环加锁也是为了保证 r++,r-- 操作的原子性,只是粒度不同。但是,要通过正确的加锁使得应该具有原子性的代码之间产生互斥来实现线程安全。

2、内存可见性

synchronized 在有限程度上可以保证内存可见性。
synchronized 大致有三个步骤:

  • 尝试加锁:加锁成功之前,清空当前线程的工作内存。

  • 执行临界区代码:读某些变量(主内存中的数据)时,保证读到的是“最新”数据(“最新”也是有限度的最新)

  • 解锁:保证把当前线程工作内存中的数据全部同步回主内存。

但是,synchronized 相当于只是对加锁解锁这两个步骤做了保证,临界区代码执行期间的数据读写不做保证(临界区代码只执行过程中,其他线程对主内存数据做了修改时,它可无法保证)

3、代码重排序

synchronized 可以给代码重排序增加一定约束
s1;s2;s3;加锁;s4;s5;s6;解锁;s7;s8;s9;

  • s1;s2;s3 之间不做保证
  • s4;s5;s6 之间不做保证
  • s7;s8;s9 之间不做保证

但是,可以保证的是:

  • s4;s5;s6 不会被重排序到加锁之前,解锁之后
  • s1;s2;s3 不会被重排序到加锁之后
  • s7;s8;s9 不会被重排序到解锁之前

4、总结

synchronized 加锁操作的作用:

  • 保证原子性:通过将应该具有原子性的代码之间产生互斥来实现线程安全(最主要的)
  • 内存可见性(了解一下)
  • 代码重排序(了解一下)

synchronized 是很早之前据出现的锁机制,比较古老,后期又进行了重新设计,现在我们写java并发编程常用 java.util.concurrent. * (juc)包下提供的工具。

三、java.util.concurrent.locks.Lock

这是链接,可以点进去看看链接: java.util.concurrent.locks.Lock

Lock是一个接口,我们常用 ReentrantLock 这个实现类???
实现类

这个Lock接口下有几个常用方法:
方法

用法:

Lock lock=new ReentrantLock();
lock.lock();
try{
	//临界区代码
}finally{
	lock.unlock();//将unlock()写到这里,确保任何情况下都会解锁。
}

1、lock()

lock():加锁,就等同于 synchronized 锁。

public class Main1 {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            lock.lock();
            System.out.println("子线程进入临界区");// 理论上,主线程不解锁(lock.unlock()),这句代码永远到达不了
        }
    }

    //主线程没有释放锁(lock.unlock();),所以不会进入子线程
    public static void main(String[] args) throws InterruptedException {
        lock.lock();
        MyThread t = new MyThread();
        t.start();
//        lock.unlock();(主线程解锁之后,就可以进入子线程了)
        t.join();
    }
}

在这个代码中,主线程对 lock 对象加锁了而且并没有释放锁,所以子线程无法对 lock 进行加锁,就永远不会有输出。

public class Main2 {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            lock.lock();
            System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);

        t.interrupt();   // 尝试让子线程停下来,但实际会徒劳无功(它对主线程没有一点点影响)

        t.join();
    }

在这端代码中,主线程尝试中断子线程(t.interrupt();),但也是徒劳无功,因为lock()它死等呀。

2、lockInterruptibly()

lockInterruptibly():加锁但允许被中断。

public class Main3 {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                lock.lockInterruptibly();
                System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
            } catch (InterruptedException e) {
                System.out.println("收到停止信号,停止运行");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);

        t.interrupt();

        t.join();
    }
}

由于 lockInterruptibly() 方法允许被中断,所以 2S 后,程序会以异常的方式结束并输出。

3、tryLock()

tryLock():加锁(没有时间限制)失败后返回false。

public class Main4 {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = lock.tryLock();
            if (b == true) {
                // 加锁成功了
                System.out.println("加锁成功");
                System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
            } else {
                System.out.println("加锁失败");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);

        t.interrupt();    // 尝试让子线程停下来,但实际会徒劳无功

        t.join();
    }
}

这段代码会输出加锁失败,因为主线程没有释放锁。但是可以看到,这个方法加锁失败后不是死等,还可以执行其他代码。

4、tryLock(long time, TimeUnit unit)

tryLock(long time, TimeUnit unit):加锁(有时间限制)失败后返回false。

public class Main5 {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = false;
            try {
                b = lock.tryLock(5, TimeUnit.SECONDS);
                if (b == true) {
                    // 加锁成功了
                    System.out.println("加锁成功");
                    System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
                } else {
                    System.out.println("5s 之后加锁失败");
                }
            } catch (InterruptedException e) {
                System.out.println("被人打断了");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(10);
//        t.interrupt();   
//        lock.unlock();
        t.join();
    }
}

上面这段代码,由于主线程休眠了10S,子线程在加锁时限时为5S,所以程序会以输出“5s 之后加锁失败”的形式正常结束。

Java程序结束的方式有两种:

  • 正常结束并返回
  • 异常结束并抛出异常

所以,三种情况会导致这段代码结束

  • 1、(超时时间内)5s之内加锁成功,正常返回,返回true
  • 2、(超时时间到了)5s之内没有加锁成功,正常返回,返回false
  • 3、(超时时间内)5s之内还没有加锁成功,但是线程被中止了,异常返回(catch捕获异常)

5、unlock()

unlock():解锁。

6、关于死锁

在上面 lock() 方法的代码演示中,就出现了死锁现象。主线程在调用 join() 方法等待子线程结束,子线程在等待主线程释放锁但主线程并没有释放锁,两个线程互相等待。但这里的死锁并不是操作系统中由于资源分配而产生的,只是现象一样而已。产生死锁后并不好查看,所以给初学者推荐一个好用的工具 jconsole 来检查多线程开发中是否产生死锁现象(查看线程运行情况)。

四、synchronized锁 VS juc下的锁

synchronized锁 juc下的锁
有加锁就有解锁(代码的书写就保证一定有锁的释放) 可能会忘记写unlock()而导致锁一致没有被释放
不灵活 书写更灵活,可以在一个方法中加锁,另一个方法中解锁
只有一种类型的锁 多种类型的锁
一直请求锁 锁的加锁策略更灵活(1、一直请求锁 2、允许中断 3、尝试请求 4、带超时的尝试请求)

总结

明天继续哦


网站公告

今日签到

点亮在社区的每一天
去签到