线程是我们在开发中经常需要用到的,所以了解线程也是很有必要,本章主要围绕线程间的通信。主要的讲解内容:
讲到线程还是先说明下进程和线程的区别:
进程:一个APP的启动前都会创建个进程,然后APP的运行基本就基于这个进程中。除非我们有意去创建新的进程,不然一个APP就只会在这个进程中完成自身所有的操作。
线程:我们开发中经常会用到,new Thread().start()去创建线程。这个线程就会和所处的进程资源共享。
说到线程间通信,那自然就有进程间通信,想了解的朋友可以看下我的另一篇文章
下进入正题。目录:
一、synchronized关键字
二、sleep/wait、wait/notify
一、synchronized关键字
- synchronized对象锁
- synchronized来实现线程间通信
- synchronized/volatile
- synchronized/lock
1.synchronized对象锁
sychronized是对象锁,表明synchronized关键字锁住的是当前的对象。如下方代码:
public class synchronizedObject {
synchronized public void methodA(){
}
public void methodB(){
synchronized (this){
}
}
}
在这个类中,methodA、methodB都用了synchronized关键字,两种写法效果是一样的。都是锁住了method这个方法。这就是synchronized的其中一种用法。
2.synchronized来实现线程间通信
先上代码:
public class testSynchronized {
public static void main(String[] args){
MyObject object = new MyObject();
ThreadA a = new ThreadA(object);
ThreadB b = new ThreadB(object);
a.start();
b.start();
}
}
class MyObject{
synchronized public void methodA(){
System.out.println("a");
}
synchronized public void methodB(){
System.out.println("b");
}
}
class ThreadA extends Thread{
private MyObject object;
public ThreadA(MyObject object){
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
class ThreadB extends Thread{
private MyObject object;
public ThreadB(MyObject object){
this.object = object;
}
@Override
public void run() {
super.run();
object.methodB();
}
}
这里我创建了MyObject类写了个带锁方法method。
再建了ThreadA和ThreadB。在main让A和B Thread同时持有MyObject对象去同时调用。
两个线程同时持有一个对象并且同时去调用,但是因为带有synchronized是对象锁,哪怕他们调用的是同一对象的不通方法,但ThreadB也必需等到ThreadA调用完A方法后才能够调用B方法。
这种通过共同拥有一个变量的方式也可以称为共享内存式的通信。
3.synchronized/volatile
int i1;
int geti1(){
return i1;
}
volatile int i2;
int geti2(){
return i2;
}
int i3;
synchronized int getI3(){
return i3;
}
上面是3种变量的写法,i1是不加关键字。i2加了volatile。i3加了synchronized。
那么三种有什么区别:
i1:在被多线程持有并且改变的时候,i1数值会不统一。也就是说这样的i1是不具备线程间通信的唯一性。为什么会有这种情况出现?因为java当做有一个主内存的机制,其实就是缓存的机制,可以保存变量当前的值。每个线程又有独立的内存,这个变量值会拷贝到线程独立内存中。所以当A线程和线程B同时更改这个值但没有通知主内存,就会造成和主内存的变量值不同。
i2:用了volatile的值是不允许线程从主内存中拷贝到自己的独立内存中,也就是说线程A或线程B想获取,都是取的主内存的i2。无论在任何线程中改变变量,其他线程都会立刻得到改变的结果。也因为这个高效的同步性,使用volatile对性能上会有所消耗。
i3:synchronized会有个监听器,如果两个线程都要用geti3方法,先获得锁的线程可使用此方法,另一个线程只能等锁解开才能获取锁调用。
获取锁后会与主内存同步数据。然后代码块就会被执行。执行完后会把任何变量的更改返回给主内存。然后释放对象锁。
volatile与synchronized区别:
volatile:只能在线程内存和主内存之间同步一个变量值。只能在变量上使用。
synchronized:在线程内存和主内存之间同步所有变量。并且可以通过锁来管理整个变量。性能上会比volatile更大的消耗。可以在变量、方法、类上。
4.synchronized/lock
类别 | synchronized | lock |
存在层次 | java的关键字,在jvm层 | 是一个类 |
锁的释放 | 1.以获取锁的线程执行完同步代码释放锁 2.线程执行异常,jvm让线程释放锁 | 需要手动代码解锁,最好在finally中释放,不然容易造成死锁 |
锁获取 | 线程执行带锁对象时判断,如果没被锁将直接获取,如果在锁状态则等待。 | 代码获取锁,线程可以不用一直等待。 |
锁状态 | 无法判断 | 可判断 |
锁类型 | 悲观锁 | 乐观锁 |
lock其实实际运用比较少,需要手动去锁和解锁,运用不当可能会造成死锁。
这里重点说下悲观锁和乐观锁区别:
悲观锁:悲观的概念就是总怕此数据正确性有问题,所以每次操作数据都要上锁,以确保每次数据都是正确且不会被别的线程修改。
乐观锁:总认为别人不会改数据,所以都不会上锁,只有在更新数据的时候会判断此间别人有没去更新这个数据。可以使用版本号等的机制去判断。
乐观锁适用于读取多的数据,悲观锁适用于经常被修改的数据。
二、sleep/wait、wait/notify
类别 | sleep | wait |
所属类 | 属于Thread类 | Object类 |
是否释放对象锁 | 不会 | 会 |
使用限制 | 任何地方都可以 | 智能在同步方法或者异步块中使用 |
效果 | 只让出了cpu不让出同步资源锁 | 让出同步资源锁、CPU |
什么时候恢复 | 在指定时间之后线程恢复运行 | notyfy方法被调用后恢复参与竞争同步资源锁,进而继续执行。 |
是否需要捕捉异常 | 是 | 否 |
sleep和wait区别直接看表格。
结合上面synchronized关键字,我们可以了解到,如果我们为了保持此数据还在自身线程中不被其他线程占用,那就可以用sleep方法。如果是此数据可以先别其他线程使用,那我们可以用wait。
wait和notify/notifyAll就很好理解了。
wait:调用后把当前锁释放让出CPU,进入一个等待的状态。
notify/notifyAll:执行该方法后,会唤醒线程,如果获得对象锁则继续执行。