1.线程同步
为什需要线程同步?当多个线程访问互斥资源(不一定是互斥资源,是需要限制访问的资源,这里举一个互斥资源的例子)时,每次只能有一个线程进行访问,这个过程叫做线程同步。譬如:你的笔记本坏了,笔记本是互斥资源要么你在用,要么给店员维修:当店员维修的时候,你不应该使用,当你使用的时候,店员不应该维修,这个场景即线程同步。
Java中实现线程同步有两种最基本方式(后续博文会探讨其他线程同步的实现),一种是使用同步代码块,一种是使用同步方法。各有各的用处:如果源码是你自己写的话,使用同步方法直接定义就是OK的,但是如果你要进行并发的操作并不是你编写的而是第三方的,你看不到源码,这个时候就要使用同步代码块咯。
同步方法:在方法原型上加上synchronized关键字即可,实例代码如下:
public class SynchronizeThreadsUsingSynchronizedMethod {
public static void main(String[] args) {
/*
* 线程同步是为了解决当多个线程同时访问互斥资源时如何保证只有一个线程在使用资源,资源可以是任何东西
*/
CallMe callMe = new CallMe();
new Thread(()->{
Caller caller = new Caller(callMe,"hello");
caller.call();
}).start();
new Thread(()->{
Caller caller = new Caller(callMe,"world");
caller.call();
}).start();
new Thread(()->{
Caller caller = new Caller(callMe,"I'm Max");
caller.call();
}).start();
/*
* 对方法进行同步之前,输出结果:
* [hello[world[I'm Max]
* ]
* ]
*/
/*
* 对方法加了同步之后,输出结果:
* [hello]
* [I'm Max]
* [world]
*/
}
}
class CallMe{
public synchronized void showMsg(String msg){
System.out.print("["+msg);
try {
//睡上这一秒就是为了在线程不同步的时候让其他线程插进来
Thread.sleep(1000);
System.out.println("]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Caller{
private CallMe callMe;
private String msg;
public Caller(CallMe callMe,String msg){
this.callMe = callMe;
this.msg = msg;
}
public void call(){
callMe.showMsg(msg);
}
}
同步代码块:格式为synchronized(objRef){},objRef为不是并发设计但是你想并发使用的对象,实例如下:
public class SynchronizeThreadsUsingSynchronizedStatements {
public static void main(String[] args) {
/*
* 有时候,因为有些类本身并不是为了并发而设计的,但是你又想并发使用,巧的是你不是该类的作者即不能修改该类的源码,那这个时候同步语句的作用就来咯
*/
CallMe2 callMe = new CallMe2();
new Thread(()->{
Caller2 caller = new Caller2(callMe,"hello");
caller.call();
}).start();
new Thread(()->{
Caller2 caller = new Caller2(callMe,"world");
caller.call();
}).start();
new Thread(()->{
Caller2 caller = new Caller2(callMe,"I'm Max");
caller.call();
}).start();
/*
* 使用同步语句前:
* [world[hello[I'm Max]
* ]
* ]
* 使用同步语句后:
* [world]
* [I'm Max]
* [hello]
*/
}
}
class CallMe2{
public void showMsg(String msg){
System.out.print("["+msg);
try {
Thread.sleep(1000);
System.out.println("]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Caller2{
private CallMe2 callMe;
private String msg;
public Caller2(CallMe2 callMe,String msg){
this.callMe = callMe;
this.msg = msg;
}
public void call(){
synchronized(callMe){
callMe.showMsg(msg);
}
}
}
2.Java中的监视器
你肯定想过这个问题:wait(),notify(),notifyAll()(进行线程间通信的方法)方法为什么被设计为Object的不可重写方法,而不是Thread类的方法呢?
回答这个问题和学习线程间通信之前,先了解一下什么是『监视器』?
监视器(monitor):相当于一个小房子,放到这个监视器小房子中的东西每次都只能由一个消费者使用,那反映到Java中,每个对象都有自己的数据和操作,有时候我们就想实现一个对象中的数据或操作在任意时候都只能由一个线程访问,那就要用到Java中的监视器了,Java在官方文档中用的也是monitor这个单词哦,所以要理解好这个monitor是个什么鬼。
如果要我们自己为每个对象都实现一个监视器太过麻烦了,Java为每个对象(不是类,是对象)都内置关联了一个监视器,同样的,如果不告诉监视器这个对象的哪个部分是被监视的,监视器是不起作用的,而要怎么告诉监视器这个对象中哪些数据和操作是要被监视的呢?是使用同步方法和同步代码块的方式。
问题又来了,一个对象中可能有多个同步代码块和同步方法,那这些同步代码块和同步方法之内应该是线程同步的,那之间呢?可不可以多个线程同时访问这个对象中不同的同步代码块或同步方法呢?答案是否定的:不能。每个对象只被关联一个监视器,而这个监视器每次只能由一个线程持有,也就是说:一个对象中无论有几个同步代码块和同步方法,他们都是一个整体,都被放到了监视器这个小屋子内,而线程对这个对象的访问是是否拿到了监视器。
验证示例代码如下:
public class TestJavaMonitor {
public static void main(String[] args) {
TestJavaMonitor_T1 t1 = new TestJavaMonitor_T1();
new Thread(()->{
t1.method1();
}).start();
new Thread(()->{
t1.method2();
}).start();
new Thread(()->{
t1.method1();
}).start();
/*
* 目的:为了检验一个对象中的不用同步块或同步方法之间是否是互斥的
* 起因:Java为每一个对象都关联了一个监视器,如果不为该监视器指明要监视的内容,则该监视器不起作用
* 使用同步块或者同步语句来告诉该对象的监视器要监视的内容
* 每个对象都只关联了一个监视器,而这个监视器可以监视多个区域(数据或操作)
* 每个进入到监视区域(同步块或者同步方法)内的线程将持有该监视器
* 但是wait之后,表示该线程被挂起,释放监视器
* notify是唤醒一个或多个(不同方法)等待持有该监视器的线程
* 监视器的作用就是该监视器下的对象资源每次只能被一个线程所访问
* 所以,同一对象中的不同同步块之间的运行同步的,因为这些同步块和同步方法中的内容是由该对象的同一个也是唯一一个监视器所监视的
* 也就是说:即使有多个线程访问该对象的不同同步块,每次也只能有一个对象访问该对象
* 更简而言之:每个对象的监视器中的内容每个时刻都只能被一个线程所访问
* 不被监视的内容就没有这个限制了
* 结论:是的,因为位于同一个监视器下
*/
// 运行结果如下:
// 这个结果并不能证明什么,但是我发誓,运行了至少20遍,都是一个方法执行完了之后,才会去执行另一个方法,如果可以多个线程同时调用对象不同的同步块,20次都是这样一个方法执行完一个方法才开始执行的可能性几乎为零
// method1...1
// method1...2
// method1...3
// method1...4
// method1...5
// method1...6
// method1...7
// method1...8
// method1...9
// method1...10
// method1...11
// method1...12
// method1...13
// method1...14
// method1...15
// method1...16
// method1...17
// method1...18
// method1...19
// method1...20
// method2...19--------------->
// method2...18--------------->
// method2...17--------------->
// method2...16--------------->
// method2...15--------------->
// method2...14--------------->
// method2...13--------------->
// method2...12--------------->
// method2...11--------------->
// method2...10--------------->
// method2...9--------------->
// method2...8--------------->
// method2...7--------------->
// method2...6--------------->
// method2...5--------------->
// method2...4--------------->
// method2...3--------------->
// method2...2--------------->
// method2...1--------------->
// method2...0--------------->
// method1...1
// method1...2
// method1...3
// method1...4
// method1...5
// method1...6
// method1...7
// method1...8
// method1...9
// method1...10
// method1...11
// method1...12
// method1...13
// method1...14
// method1...15
// method1...16
// method1...17
// method1...18
// method1...19
// method1...20
}
}
class TestJavaMonitor_T1{
int k = 0;
public synchronized void method1(){
for(int i = 0;i<20;i++){
k++;
System.out.println("method1..."+k);
}
}
public synchronized void method2(){
for(int i = 0;i<20;i++){
k--;
System.out.println("method2..."+k+"--------------->");
}
}
//没有被监视的内容
public void method3(){
for(int i = 0;i<20;i++){
k--;
System.out.println("method3..."+k);
}
}
}