JUC------共享模型------管程

概念

什么是管程

管程(Monitor,直译是”监视器“的意思)是一种操作系统中的同步机制,它的引入是为了解决多线程或多进程环境下的并发控制问题。
翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类支持并发访问,是线程安全的
参考: https://www.cnblogs.com/xidongyu/p/10891303.html

临界区

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    1、多个线程读共享资源其实也没有问题
    2、在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量
    本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
    时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
    的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized

语法

synchronized(对象) // 线程1, 线程2(blocked)
{
     临界区
}
@Slf4j
public class SynchronizedTest1 {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (SynchronizedTest1.class){
                    count++;
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                
                synchronized (SynchronizedTest1.class){
                    count--;
                }
            }
        }, "t2");


        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断。
为了加深理解,请思考下面的问题

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
    答: 个人认为是锁住了整个循环,第二个循环只能等第一个循环执行完才能执行,执行顺序上相当于没有使用多线程,代码一行一行执行。
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
    答:有线程安全问题,相当于并没有锁。
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象
    答: 有线程安全问题,相当于并没有锁。

synchronized 的位置

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锁的到底是什么?
参考这个回答,自认为还是不错的

https://blog.csdn.net/YangYF1997/article/details/117164944?spm=1001.2101.3001.6650.9&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-9-117164944-blog-122815348.235%5Ev43%5Epc_blog_bottom_relevance_base9&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-9-117164944-blog-122815348.235%5Ev43%5Epc_blog_bottom_relevance_base9&utm_relevant_index=16

synchronized锁住同一个对象,线程才会互斥阻塞,才会线程安全。

线程八锁 练习

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

  • 当synchronized修饰一个static方法时,多线程下,获取的是类锁(即Class本身,注意:不是实例),
    作用范围是整个静态方法,作用的对象是这个类的所有对象。
  • 当synchronized修饰一个非static方法时,多线程下,获取的是对象锁(即类的实例对象),
    作用范围是整个方法,作用对象是调用该方法的对象

----------------结论: 类锁和对象锁不同,它们之间不会产生互斥

一、

//结果是 先1后2 或者先2后1 
@Slf4j
public class Test1 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -> {
            //n1对于a来讲就是this
            n1.a();
        }, "t1").start();

        new Thread(() -> {
            //n1对于b来讲也是this 和 n1.a()中n1一样都是this
            n1.b();
        }, "t2").start();

    }

}

@Slf4j
class Number {
    /**
     *  public synchronized void a() {} 相当于 synchronized (this) {},所以其锁住的是number实例对象本身
     */
    public synchronized void a() {
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}

二、

/**
 * 程序执行 1s后先打印1后打印2,或者程序先打印2,1s后打印1 
 * @author Spider Man
 * @date 2024-05-10 15:50
 */
public class Test2 {
    public static void main(String[] args) {
        Number2 n2 = new Number2();
        new Thread(() -> {
            n2.a();
        }, "t1").start();

        new Thread(() -> {
            n2.b();
        }, "t2").start();

    }
}

@Slf4j
class Number2 {

    public synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}

三、

/**
 *       首先,该案例启用了3个线程,但是线程 t3 没有上锁,t1和t2共用一把锁,所以可以把其看成两个梯队,第一梯队两个线程,第二梯队一个线程,
 *   而且t3总是在第一梯队中,要么在第一梯队的第一个,要么在第一梯队在第二个
 *   所以其运行情况可分为三种:
 *      t3 t2 1s后t1      t2 t3 1s后t1    t3  1s后t1 t2
 *   这个案例主要在于 c() 方法没有上锁,所以不用排队总是会被第一次调用,
 * @author Spider Man
 * @date 2024-05-10 16:00
 */
public class Test3 {
    public static void main(String[] args) {
        Number3 n3 = new Number3();
        new Thread(() -> {
            n3.a();
        }, "t1").start();

        new Thread(() -> {
            n3.b();
        }, "t2").start();

        new Thread(() -> {
            n3.c();
        }, "t3").start();
    }
}
@Slf4j
class Number3 {

    public synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

    public void c(){
        log.debug("3");
    }
}

四、

/**
 *  synchronized 是由不同的number对象加锁,所以两个线程不会阻塞互斥
 *  永远都是  t2 1s后t1
 * @author Spider Man
 * @date 2024-05-10 16:18
 */
@Slf4j
public class Test4 {
    public static void main(String[] args) {
        Number4 n1 = new Number4();
        new Thread(() -> {
            n1.a();
        }, "t1").start();

        Number4 n2 = new Number4();
        new Thread(() -> {
            n2.b();
        }, "t2").start();

    }
}
@Slf4j
class Number4 {

    public synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

}

五、

/**
 *  因为a()方法由static,所以其锁住的是类对象, b()方法是非static方法,锁住的是实例对象
 *  所以两个线程不是同一个锁,所以都是单独运行,但因为t1会先睡1s,所以视觉上总是t2先打印之后打印t1
 * @author Spider Man
 * @date 2024-05-10 17:14
 */
public class Test5 {
    public static void main(String[] args) {
        Number5 n1 = new Number5();
        new Thread(() -> {
            n1.a();
        }, "t1").start();

        new Thread(() -> {
            n1.b();
        }, "t2").start();
    }
}

@Slf4j
class Number5 {

    public static synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

}

六、

/**
 * 对类对象加锁,类对象整个内存只有一份,所以其用的锁是同一个对象
 * 所以其结果是 1s后 t1 t2 或者 t2 1s后t1
 * @author Spider Man
 * @date 2024-05-10 17:14
 */
public class Test6 {
    public static void main(String[] args) {
        Number6 n1 = new Number6();
        new Thread(() -> {
            n1.a();
        }, "t1").start();

        new Thread(() -> {
            n1.b();
        }, "t2").start();
    }
}

@Slf4j
class Number6 {

    // 对类对象加锁,类对象整个内存只有一份,所以其用的锁是同一个对象
    public static synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
    }

    public static synchronized void b() {
        log.debug("2");
    }

}

七、

/**
 * 两个线程不是同一个锁,所以都是单独运行,但因为t1会先睡1s,所以视觉上总是t2先打印之后打印t1
 * @author Spider Man
 * @date 2024-05-10 17:14
 */
public class Test7 {
    public static void main(String[] args) {
        Number7 n1 = new Number7();
        Number7 n2 = new Number7();
        new Thread(() -> {
            n1.a();
        }, "t1").start();

        new Thread(() -> {
            n2.b();
        }, "t2").start();
    }
}

@Slf4j
class Number7 {

    public static synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

}

八、

/**
 * 其结果是 1s后 t1 t2 或者 t2 1s后t1
 * @author Spider Man
 * @date 2024-05-10 17:14
 */
public class Test8 {
    public static void main(String[] args) {
        Number8 n1 = new Number8();
        Number8 n2 = new Number8();
        new Thread(() -> {
            n1.a();
        }, "t1").start();

        new Thread(() -> {
            n2.b();
        }, "t2").start();
    }
}

@Slf4j
class Number8 {

    public static synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
    }

    public static synchronized void b() {
        log.debug("2");
    }

}

线程安全分析

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

  • 成员变量
    有可能会发生线程安全。如果只有读操作,则线程安全,如果有读还有写,(临界区),就是线程不安全的。

  • 局部变量
    局部变量是线程安全的,因为它是线程私有的,不满足共享条件。

    原理是,每次方法调用对应着一个栈帧的创建,局部变量保存在栈帧的局部变量表中,而栈是线程私有的。

    但,局部变量引用的对象则不一定:
    解释一:
    如果该对象没有跨越方法的作用范围,那么它是线程安全的
    如果该对象跨越了方法的作用范围,它就不是线程安全的。

    解释二:
    如果该对象没有逃离方法的作用访问,它是线程安全的
    如果该对象逃离(return)方法的作用范围,需要考虑线程安全

    解释三:
    有一种情况就是子类继承父类,重写父类中的方法,在重写的方法中开一个线程去操作共享变量,这样就会有线程安全问题

    这个时候就要通过 private、final 这些修饰符去限制了

视频举例:https://www.bilibili.com/video/BV16J411h7Rd?p=66&vd_source=4085910f7c5c4dddcc04446ebf3aed6b

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

线程安全类方法的组合

比如:这就是线程安全的,两个线程调用同一个实例table的同一个方法put。

Hashtable table = new Hashtable();

new Thread(()->{
 table.put("key", "value1");
}).start();

new Thread(()->{
 table.put("key", "value2");
}).start();

也就是说他们每个方法是原子的,被sychronized修饰,但多个方法的组合不是原子的,会发生线程安全问题。
比如:下面代码中hashtable的get方法和put方法虽然都是线程安全的,但是其是单个方法的原子性的线程安全,也就是说其源码中sychronized只单独修饰了put或get方法,但现在示例中两个线程的都用到了get和put,所以第一个线程get判断时,cpu很可能会把时间片分给了第二个线程,直到第二个线程运行完才把时间片分给第一个线程去执行最后的put方法。

public class HashTableTest2 {
    public static void main(String[] args) throws InterruptedException {
        Hashtable<String, String> hashtable = new Hashtable<>();


        Thread t1 = new Thread(() -> {
            if (hashtable.get("1")==null){
                hashtable.put("1", "1");
            }
        });

        Thread t2 = new Thread(() -> {
            if (hashtable.get("1")==null){
                hashtable.put("1", "2");
            }
        });


        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(hashtable);
    }
}

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
因为String 的 replace和substring 最终都是重新创建了个string 对象(new String)

线程安全—示例分析

前提知识:servlet在tomcat中只有一个实例
例1、

public class MyServlet extends HttpServlet {
 // 是否安全?  ---不安全
 Map<String,Object> map = new HashMap<>();
 // 是否安全?  --安全
 String S1 = "...";
 // 是否安全?  --安全
 final String S2 = "...";
 // 是否安全?  --不安全
 Date D1 = new Date();
 // 是否安全?  --不安全
 final Date D2 = new Date();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 
 // 使用上述变量
 }
 
}

例2、
线程不安全,因为UserServiceImpl中成员变量count值被改变。

public class MyServlet extends HttpServlet {
    // 是否安全?
    private UserService userService = new UserServiceImpl();
 
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
 
   }
 
}


public class UserServiceImpl implements UserService {
 // 记录调用次数
    private int count = 0;
 
    public void update() {
        // ...
        count++;
     } 
}

例3、
前提知识:
Spring默认使用单例模式管理Bean,简单来说就是在IoC容器中,默认情况下一个类只会存在一个它的实例,即对应的每个(标注了@Component等注解的)类只会被实例化一次。

答案:线程不安全,spring 中的对象默认是单例的,会被共享,所以针对MyAspect类中的start成员变量也是会被共享的,又因为before和after方法都对start作了写的操作,所以其是线程不安全的。

@Aspect
@Component
public class MyAspect {
 // 是否安全?
 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、线程安全 三层结构中的典型调用

public class MyServlet extends HttpServlet {
	 // 是否安全    --安全  UserServiceImpl 中 UserDao 成员变量没有被执行写的操作
 	private UserService userService = new UserServiceImpl();
 
	 public void doGet(HttpServletRequest request, HttpServletResponse response) {
	    userService.update(...);
	 
	 }


}


public class UserServiceImpl implements UserService {
 // 是否安全    --安全    UserDaoImpl中没有成员变量
	 private UserDao userDao = new UserDaoImpl();
 
	 public void update() {
	   userDao.update();
	 }
 
}

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、 线程不安全,根例4的区别在于userDao中Connection 在成员变量中

public class MyServlet extends HttpServlet {
	 // 是否安全   --不安全
	 private UserService userService = new UserServiceImpl();
	 
	 public void doGet(HttpServletRequest request, HttpServletResponse response) {
		 userService.update(...);
	 }
}
public class UserServiceImpl implements UserService {
	 // 是否安全  --不安全
	 private UserDao userDao = new UserDaoImpl();
	 
	 public void update() {
	 	userDao.update();
	 }
}

public class UserDaoImpl implements UserDao {
 	// 是否安全  不安全
	 private Connection conn = null;
	 public void update() throws SQLException {
		 String sql = "update user set password = ? where username = ?";
		 conn = DriverManager.getConnection("","","");
		 // ...
		 conn.close();
	 }
}

例6
线程安全,虽然UserDaoImpl 中 Connection 是成员变量,但因为UserServiceImpl 中的update方法每次调用都会创建一个新的UserDaoImpl对象,其是局部变量,所以是线程安全的

public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}

public class UserServiceImpl implements UserService { 
 public void update() {
 UserDao userDao = new UserDaoImpl();  ------关键点
 userDao.update();
 }
}

public class UserDaoImpl implements UserDao {
 // 是否安全  
 private Connection = null;
 public void update() throws SQLException {
 String sql = "update user set password = ? where username = ?";
 conn = DriverManager.getConnection("","","");
 // ...
 conn.close();
 }
}

例7
线程不安全,其中 foo 的行为是不确定的(比如若有个类继承Test并重写其foo方法,在重写的方法中又新启了个线程对sdf进行操作,虽然SimpleDateFormat 是局部变量,但是其引用逃逸,泄露),可能导致不安全的发生,被称之为外星方法

public abstract class Test {
 
	 public void bar() {
	 	 // 是否安全
		 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		 foo(sdf);
	 }
 
	 public abstract foo(SimpleDateFormat sdf);
 
 
	 public static void main(String[] args) {
		 new Test().bar();
	 }
}


public void foo(SimpleDateFormat sdf) {
	 String dateStr = "1999-10-11 00:00:00";
	 for (int i = 0; i < 20; i++) {
		 new Thread(() -> {
		 	try {
			 sdf.parse(dateStr);
			 } catch (ParseException e) {
				 e.printStackTrace();
			 }
		 }).start();
	 }
}

例8、

虽然使用了 synchronized 关键字来保护对共享变量 i 的访问,但是这里对共享变量 i 的同步锁锁定的是 Integer 对象,而不是 i 的实际值。由于 Integer 是不可变对象,每次对 i 进行自增操作时,实际上是创建了一个新的 Integer 对象,因此每个线程获得的锁都是不同的对象,无法保证线程安全

@Slf4j
public class Test4 {
    private static Integer i = 0;

    public static void main(String[] args) {
        List<Thread> list = new ArrayList<>();

        for (int j = 0; j < 2; j++) {
            Thread thread = new Thread(() -> {
                for (int k = 0; k < 5000; k++) {
                    synchronized (i) {
                        i++;
                    }
                }
            }, "" + j);
            list.add(thread);
        }


        list.stream().forEach(t -> t.start());
        list.stream().forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        log.debug("{}",i);

    }


}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值