《JUC并发编程 - 高级篇》03 - 共享对象之管程 上篇(共享带来的问题 | synchronized | 线程八锁 | 线程安全类)(二)

简介: 《JUC并发编程 - 高级篇》03 - 共享对象之管程 上篇(共享带来的问题 | synchronized | 线程八锁 | 线程安全类)

3.3 方法上的 synchronized

//成员方法上加synchronized,锁对象是当前对象this
class Test{
    public synchronized void test() {
    }
}
//等价于
class Test{
    public void test() {
        synchronized(this) {
        }
    }
}
//静态方法上加锁,锁对象是类对象
class Test{
    public synchronized static void test() {
    }
}
//等价于
class Test{
    public static void test() {
        synchronized(Test.class) {
        }
    }
}

不加 synchronized 的方法

不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

3.4 “线程八锁”

其实就是考察 synchronized 锁住的是哪个对象

情况一

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();//此时synchronized 锁住的是同一个锁对象
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a(){
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
}

结果:打印1 2 2 1

88c3fbaaae5d74717d2bde833d322102.png

0b373035407157c1a4a74600f86896af.png

情况二

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a(){
        sleep(1);
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
}

结果:1s后打印 1 2先打印2 , 1s后打印1

情况三

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.c();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a(){
        sleep(1);
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
    public synchronized void c(){
        log.debug("3");
    }
}

结果:先打印3, 1s后打印1 2先打印 2 3, 1s后打印1先打印3 2, 1s后打印1

情况四

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        Number number2 = new Number();
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number2.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a(){
        sleep(1);
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
}

结果:先打印2, 1s后打印 1

情况五

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a(){//锁的是类对象
        sleep(1);
        log.debug("1");
    }
    public synchronized void b(){//锁的是this对象
        log.debug("2");
    }
}

结果:先打印2, 1s后打印 1

情况六

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a(){
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b(){
        log.debug("2");
    }
}

结果:先打印2, 1s后打印 1

情况八

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        Number number2 = new Number();
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number2.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a(){
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b(){
        log.debug("2");
    }
}

结果:1s后打印 1 2先打印2 , 1s后打印1

3.5 变量的线程安全分析

成员变量和静态变量是否线程安全?


如果它们没有共享,则线程安全

如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

如果只有读操作,则线程安全

如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?


局部变量是线程安全的

但局部变量引用的对象则未必

如果该对象没有逃离方法的作用访问,它是线程安全的

如果该对象逃离方法的作用范围,需要考虑线程安全

3.5.1 局部/成员 变量线程安全分析

**情况一:**普通局部变量是线程安全的

public static void test1() {
    int i = 10;
    i++;
}

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

public static void test1();
    descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
          Code:
            stack=1, locals=1, args_size=0
            0: bipush 10
            2: istore_0
            3: iinc 0, 1
            6: return
          LineNumberTable:
            line 10: 0
            line 11: 3
            line 12: 6
          LocalVariableTable:
            Start Length Slot Name Signature
              3   4      0  i      I

ca7926659a4ee83864011a69c0f04188.png

**情况二:**被共享的成员变量的线程安全

//被共享的成员变量的线程安全举例
//共享变量有读写操作
public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}
class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            method2();
            method3();
        }
    }
    private void method2() {
        list.add("1");
    }
    private void method3() {
        list.remove(0);
    }
}

运行结果:

Exception in thread "Thread2" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
  at java.util.ArrayList.rangeCheck(ArrayList.java:657)
  at java.util.ArrayList.remove(ArrayList.java:496)
  at cn.itcast.n4.ThreadUnsafe.method3(TestThreadSafe.java:32)
  at cn.itcast.n4.ThreadUnsafe.method1(TestThreadSafe.java:23)
  at cn.itcast.n4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:13)
  at java.lang.Thread.run(Thread.java:748)

分析:

  • 两次add的时候出了问题,add的时候数组长度会增加,两线程同时操作,字节码指令会执行会出现覆盖,只能加一个长度,remove两次就越界了.
new Thread(() -> {
    list.add("1");  // 时间1. 会让内部 size ++
    list.remove(0); // 时间3. 再次 remove size-- 出现角标越界
}, "t1").start();
new Thread(() -> {
    list.add("2");  // 时间1(并发发生). 会让内部 size ++,但由于size的操作非原子性,  size 本该是2,但结果可能出现1
    list.remove(0); // 时间2. 第一次 remove 能成功, 这时 size 已经是0
}, "t2").start();

例: t1获取集合的size然后加1 将值付给它, 如果获取size后 还未进行++操作。 此时,t1让出cpu 另外一个线程获取到了size依旧是0 然后t2进行size++, 最终导致两个线程的 size++执行后,size只加了一次。同理elementData[size++] = e 也会出现值覆盖问题。从而导致向集合中只add一个元素 却进行了两次remove,出现下标越界异常。


无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量


b24bebe3354816921092491d504f4f1b.png


**情况三:**局部变量引用的对象 不一定是线程安全的

将 list 修改为局部变量


//局部变量引用的对象是线程安全的情况
public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadSafe test = new ThreadSafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        // System.out.println(1);
        list.remove(0);
    }
}

那么就不会有上述问题了

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

eb5e24b20c657eb0b9d1bd8dd74d8d8f.png

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?


情况1:有其它线程调用 method2 和 method3


无线程安全问题,因为调用method1的线程和直接调用method3的线程 传入的不是同一个list.


情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即


//局部变量引用的对象不是线程安全
//此处省略测试类...
class ThreadSafe {
    public void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

结果:

5bfee4e8df9a86473f97c64c1cb4744f.png

结论:


方法的访问修饰 符是有一定意义的,在一定程度上可以保护线程安全。 private可以限制子类不能重写父类的方法 。public 方法如果不想让子类影响其行为 可以使用final进行修饰


从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】


不想让子类改变我的行为 那我就把它保护起来 ,通过 private 或 final


使用private 和final 修改后


9241bf7d94fb17c908a47fca8a3a9bf1.png


3.5.2 实例分析

例1:


public class MyServlet extends HttpServlet {// Myservlet只有一份,里面的成员变量/方法会被共享
    // 是否安全? NO
    Map<String,Object> map = new HashMap<>();
    // 是否安全?Yes
    String S1 = "...";
    // 是否安全? Yes
    final String S2 = "...";
    // 是否安全?不是
    Date D1 = new Date();
    // 是否安全?不是  加上final只能说明D2这个引用的指向不会再发生变化,但是创建的Date对象里面的属性依旧可以发生变化的
    final Date D2 = new Date();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    }
}

例2

public class MyServlet extends HttpServlet {
    // 是否安全? No
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
         userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 记录调用次数 No
    private int count = 0;
    public void update() {
        // ...
        count++;
    }
}

例3

@Aspect
@Component
public class MyAspect {//MyAspect默认是单例的,里面的成员变量会被共享. 可能会出现线程安全问题.
    // 是否安全? No
    private long start = 0L;
    @Before("execution(* *(..))")//这里写这俩方法是为了计算方法的执行时间
    public void before() {
        start = System.nanoTime();
    }
  @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}//改进:用环绕通知来改进,把成员变量改为局部变量

例4 (可以从下往上分析哦)

//3.本类也是线程安全的
public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
    }
}
//2.本类也是线程安全的,因为里面虽然有共享变量UserDao,但是UserDao没有可变的属性
public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();
    public void update() {
      userDao.update();
    }
}
//1.没有成员变量,所以本类一定是线程安全的
public class UserDaoImpl implements UserDao {
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // 是否安全
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}

例5

//本类是线程不安全的
public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
    }
}
//本类是线程不安全的,因为UserDao是被共享的,里面有可变的属性.
public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();
    public void update() {
      userDao.update();
    }
}
//有被共享的成员变量,所以本类是线程不安全的.
//举例:线程1刚创建了一个连接,线程2就执行了close(),关闭了连接.
public class UserDaoImpl implements UserDao {
    //是否安全 No
    private Connection conn = null;
    public void update() throws SQLException{
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","")
         //...
        conn.close();
    }
}
相关文章
|
11天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
45 0
|
11天前
|
Java
并发编程之线程池的应用以及一些小细节的详细解析
并发编程之线程池的应用以及一些小细节的详细解析
17 0
|
6天前
|
SQL 开发框架 .NET
高级主题:Visual Basic 中的多线程和并发编程
【4月更文挑战第27天】本文深入探讨了Visual Basic中的多线程和并发编程,阐述了其基本概念,如何使用`System.Threading.Thread`类创建线程,以及借助`ThreadPool`、`Monitor`和`SyncLock`进行同步管理。文章还提到了多线程编程面临的挑战如竞态条件、死锁和资源竞争,并介绍了VB的异步编程、TPL和并发集合等高级技术。通过实例展示了多线程在文件处理、网络通信和图像处理中的应用,并给出了多线程编程的最佳实践。总之,理解并掌握VB的多线程和并发编程能有效提升应用程序的性能和响应能力。
|
6天前
|
安全 Java 开发者
【JAVA】哪些集合类是线程安全的
【JAVA】哪些集合类是线程安全的
|
1天前
|
存储 缓存 前端开发
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
10 3
|
3天前
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
|
3天前
|
安全 调度 Swift
【Swift开发专栏】Swift中的多线程与并发编程
【4月更文挑战第30天】本文探讨Swift中的多线程与并发编程,分为三个部分:基本概念、并发编程模型和最佳实践。介绍了线程、进程、并发与并行、同步与异步的区别。Swift的并发模型包括GCD、OperationQueue及新引入的结构体Task和Actor。编写高效并发代码需注意任务粒度、避免死锁、使用线程安全集合等。Swift 5.5的并发模型简化了异步编程。理解并掌握这些知识能帮助开发者编写高效、安全的并发代码。
|
4天前
|
安全 算法 关系型数据库
线程安全--深入探究线程等待机制和死锁问题
线程安全--深入探究线程等待机制和死锁问题
|
4天前
|
缓存 安全 Java
多线程--深入探究多线程的重点,难点以及常考点线程安全问题
多线程--深入探究多线程的重点,难点以及常考点线程安全问题
|
4天前
|
安全 Java 开发者
构建高效微服务架构:后端开发的新范式Java中的多线程并发编程实践
【4月更文挑战第29天】在数字化转型的浪潮中,微服务架构已成为软件开发的一大趋势。它通过解耦复杂系统、提升可伸缩性和促进敏捷开发来满足现代企业不断变化的业务需求。本文将深入探讨微服务的核心概念、设计原则以及如何利用最新的后端技术栈构建和部署高效的微服务架构。我们将分析微服务带来的挑战,包括服务治理、数据一致性和网络延迟问题,并讨论相应的解决方案。通过实际案例分析和最佳实践的分享,旨在为后端开发者提供一套实施微服务的全面指导。 【4月更文挑战第29天】在现代软件开发中,多线程技术是提高程序性能和响应能力的重要手段。本文通过介绍Java语言的多线程机制,探讨了如何有效地实现线程同步和通信,以及如