场景
Java中Thread类的常用API以及使用示例:
Java中Thread类的常用API以及使用示例_霸道流氓气质的博客
在上面的基础上,学习线程同步的相关概念。
数据不一致问题引入
模拟一个营业厅叫号机程序
package com.ruoyi.demo.threadsafe;
/**
* 模拟营业大厅叫号程序,每次会不一样的发现:某个号码被略过、某个号码被多次显示、号码超过了最大值500
*/
public class TicketWindowRunable implements Runnable{
private int index = 1;
private final static int MAX = 500;
@Override
public void run() {
while (index <= MAX)
{
System.out.println(Thread.currentThread()+"的号码:"+(index++));
}
}
public static void main(String[] args) {
final TicketWindowRunable task = new TicketWindowRunable();
Thread windowThread1 = new Thread(task,"1号窗口");
Thread windowThread2 = new Thread(task,"2号窗口");
Thread windowThread3 = new Thread(task,"3号窗口");
Thread windowThread4 = new Thread(task,"4号窗口");
windowThread1.start();
windowThread2.start();
windowThread3.start();
windowThread4.start();
}
}
多次运行上面程序,每次都会不一样。
可以通过输出的行数明显发现不一样,主要有三个问题:
某个号码被略过、某个号码被多次显示、号码超过最大值
注:
博客:霸道流氓气质的博客-C#,架构之路,SpringBoot领域博主 关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
实现
1、出现上述不一样的原因是线程的执行是由CPU时间片轮询调度的,CPU调度器在将执行权交给各个线程时,多个线程对index变量(共享变量/资源)
同时操作引起的。
要解决这个问题需要使用synchronized关键字,其提供了一种排他机制,也就是在同一时间只能有一个线程执行某些操作。
2、将上面的叫号程序修改
package com.ruoyi.demo.threadsafe;
/**
* 模拟营业大厅叫号程序
*/
public class TicketWindowRunableWithSync implements Runnable{
private int index = 1;
private final static int MAX = 500;
private final static Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX){
while (index <= MAX)
{
System.out.println(Thread.currentThread()+"的号码:"+(index++));
}
}
}
public static void main(String[] args) {
final TicketWindowRunableWithSync task = new TicketWindowRunableWithSync();
Thread windowThread1 = new Thread(task,"1号窗口");
Thread windowThread2 = new Thread(task,"2号窗口");
Thread windowThread3 = new Thread(task,"3号窗口");
Thread windowThread4 = new Thread(task,"4号窗口");
windowThread1.start();
windowThread2.start();
windowThread3.start();
windowThread4.start();
}
}
此时程序运行多次,不会出现数据不一致的问题。
3、线程堆栈分析
synchronized关键字提供了一种互斥机制,在同一时刻,只能有一个线程访问同步资源。下面是一个简单示例
package com.ruoyi.demo.threadsafe;
import java.util.concurrent.TimeUnit;
public class Mutex {
private final static Object MUTEX = new Object();
public void accessResource(){
synchronized (MUTEX){
try {
//TimeUnit.MINUTES.sleep(10);
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
for (int i = 0; i < 5 ; i++) {
new Thread(mutex::accessResource).start();
}
}
}
上面定义了一个方法accessResource,使用同步代码块的方式对accessResource进行同步,同时定义5个线程调用accessResource方法。
由于同步代码块的互斥性,只能有一个线程获取了mutex monitor的锁,其他线程只能进入堵塞状态。
4、使用JConsole工具监控
找到jdk的目录下bin下jconsole.exe,双击运行
新建连接-本地进程-上面的Mutex选中-连接-接受不安全的连接-线程-Thread-0到4就是上面的5个线程
当设置休眠10分钟的时候可以看到,Thread-0在sleep
其它的则是进入了BLOCKED状态。
5、使用jps和jstack打印线程堆栈信息
通过上面的jconsole可以获取到其pid,也可以直接在cmd中输入
jps
获取所有的pid之后,再输入
jstack pid号
打印进程的线程堆栈信息
看到与上面的jconsole的效果一致。
6、规则与注意事项
规则:
每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得。
释放对monitor的所有权的前提是你曾经获取到了所有权。
注意问题:
①与monitor关联的对象不能为空。
private final static Object MUTEX = null;
②synchronized作用域太大。
如果将其作用在run方法上,则丧失了并发的优势。
③不同的monitor企图锁相同的方法。
package com.ruoyi.demo.threadsafe;
public class ErrorMutexDemo implements Runnable {
private final Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX)
{
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(ErrorMutexDemo::new).start();
}
}
}
上面的代码每个线程争抢的monitor关联引用都是彼此独立的,因此不能起到互斥的作用。
④多个锁的交叉导致死锁
7、This Monitor与Class Monitor介绍
ThisMonitor
两个方法method1和method2都被synchronized关键字修饰,启动两个线程分别访问method1和method2
package com.ruoyi.demo.threadsafe;
import java.util.concurrent.TimeUnit;
import static java.lang.Thread.currentThread;
public class ThisMonitor {
public synchronized void method1(){
System.out.println(currentThread().getName()+"enter to method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method2(){
System.out.println(currentThread().getName()+"enter to method2");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
//注意这里调用了构造方法public Thread(Runnable target, String name)
//Target——线程启动时调用其run方法的对象。如果为空,则调用此线程的run方法。
new Thread(thisMonitor::method1,"T1").start();
new Thread(thisMonitor::method2,"T2").start();
}
}
运行之后的结果为
此时用jstack分析
可以得出,synchronized关键字同步类的不同实例方法,争抢的是同一个monitor的lock,而与之相关联的引用则是ThisMonitor的实例
引用。
Classmonitor
有两个类方法(静态方法)分别使用synchronized对其进行同步
package com.ruoyi.demo.threadsafe;
import java.util.concurrent.TimeUnit;
import static java.lang.Thread.currentThread;
public class ClassMonitor {
public static synchronized void method1()
{
System.out.println(currentThread().getName()+"enter to method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void method2()
{
System.out.println(currentThread().getName()+"enter to method2");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(ClassMonitor::method1,"T1").start();
new Thread(ClassMonitor::method2,"T2").start();
}
}
运行之后的效果
使用jstack分析之后
由此可知,用synchronized同步某个类的 不同静态方法争抢的也是同一个monitor的lock。
以上代码和示例参考《Java高并发编程详解》,建议阅读原书。