概念
什么是管程
管程(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来讲就是thisn1.a();}, "t1").start();new Thread(() -> {//n1对于b来讲也是this 和 n1.a()中n1一样都是thisn1.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);}}