《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();
    }
}
相关文章
|
15天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
41 4
|
1月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
24 2
|
1月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
32 0
|
1月前
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
26 0
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
25天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
25天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
25天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
25天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1
|
25天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
34 1

热门文章

最新文章