JAVA多线程Synchronized关键字
Test19_Thread
packagecom.ygtq.review.basic;
这里的程序根本不能体现单线程与多线程的差别,无论是单线程还是多线程都会交替输出.MyClass类中的count++计算使用了同步机制,于是不允许2个线程同时对myClass对象的count进行计算,但是这里同步代码非常短,当线程A执行完count++并输出后该同步代码块就结束了,释放了对象锁,此时其他线程就可以争夺myClass,此时的线程有threadA和threadB,于是他们都有机会夺取myClass对象的对象锁进行访问。
正确的做法是:如Test20.java中所写,在MyCLass类中进行循环,从而可以体现线程对该myClass占用了较长时间,在此时间内其他线程无法占用。即在线程案例中,循环和休眠总是要放在受synchronized同步保护的资源处
public classTest19_Thread {
public static void main(String[] args){
Thread threadA=new Thread(newThreadA());
threadA.start();
Thread threadB=new Thread(newThreadB());
threadB.start();
}
}
//可以公共访问的资源
class MyClass{
static int count;
String name;
public void myMethod(){
synchronized (this) {
StringthreadName=Thread.currentThread().getName();
System.out.println(threadName+" "+(count++));
}
}
}
//线程1
class ThreadAimplements Runnable{
public void run() {
MyClass class1=new MyClass();
for (int i = 0; i <5; i++) {
class1.myMethod();
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ThreadBimplements Runnable{
public void run() {
MyClass class1=new MyClass();
for (int i = 0; i <5; i++) {
class1.myMethod();
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
Test20_Thread
packagecom.ygtq.review.basic;
线程2 (线程1和线程2操作的是相同的资源,myClass对象)synchronize同步时只能对一个对象进行同步,即只对一个MyClass对象进行同步,即如果多个线程操作的是同一个myClass对象则会同步,如果操作的不是同一个对象,那么不会同步,理解:synchronize同步块必然属于某个类(对象),那么synchronize只是不允许多个线程同时操作同一个受synchronized保护的对象,如果几个线程操作的是不同的对象实例,是可以并发的,例如这里ThreadA和ThreadB中操作的myClass对象是不同的,即在这里2个线程操作的对象是2个不同的对象通过ThreadthreadA=new Thread(new ThreadA(myClass),"线程A")ThreadthreadB=new Thread(new ThreadA(myClass),"线程B")threadA.start();threadB.start();可以起2个线程,这是两个不同的线程,只不过执行的逻辑相同而已;但是继承Thread的方式创建线程不能同时起2个线程
public classTest20_Thread {
public static void main(String[] args){
MyClass myClass=newMyClass();//使用的资源对象是myClass
Thread threadA=new Thread(newThreadA(myClass),"线程A");
//MyClass myClass2=newMyClass();//使用的资源对象是myClass2
Thread threadB=new Thread(newThreadA(myClass),"线程B");
threadA.start();
threadB.start();
ThreadC threadC=newThreadC();
threadC.start();
threadC.start();
}
}
class MyClass{
int count;
String name;
public void myMethod(){
synchronized (this) {
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i< 5; i++) {
System.out.println(threadName+" "+(count++));
try {
Thread.sleep(1000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}
}
}
//线程1
class ThreadAimplements Runnable{
MyClass class1;
public ThreadA(MyClass myClass) {
this.class1=myClass;
}
public void run() {
class1.myMethod();
}
}
//线程2 (线程1和线程2操作的是相同的资源,myClass对象)
class ThreadBimplements Runnable{
MyClass class2;
public ThreadB(MyClass myClass) {
this.class2=myClass;
}
public void run() {
class2.myMethod();
}
}
class ThreadCextends Thread{
public void run() {
System.out.println("这是线程C");
}
}
Test21_Thread
packagecom.ygtq.review.basic;
//当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块
public classTest21_Thread {
public static void main(String[] args){
MyClass myClass=new MyClass();//使用的资源对象是myClass
Thread threadA=new Thread(newThreadA(myClass),"线程A");
//MyClass myClass2=newMyClass();//使用的资源对象是myClass2
Thread threadB=new Thread(newThreadB(myClass),"线程B");
threadA.start();
threadB.start();
}
}
class MyClass{
int count;
String name;
//这是一个进行同步的方法
public void myMethod(){
synchronized (this) {
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i< 5; i++) {
System.out.println(threadName+" "+(count++));
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
//这是一个非同步的方法
public void myMethod2(){
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i < 5;i++) {
System.out.println(threadName+": 这是非同步的方法");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
//线程1
class ThreadAimplements Runnable{
MyClass class1;
public ThreadA(MyClass myClass) {
this.class1=myClass;
}
public void run() {
class1.myMethod();
}
}
//线程2 (线程1和线程2操作的是相同的资源,myClass对象)
class ThreadBimplements Runnable{
MyClass class2;
public ThreadB(MyClass myClass) {
this.class2=myClass;
}
public void run() {
//调用的是MyClass中的非同步方法
class2.myMethod2();
}
}
class ThreadCextends Thread{
public void run() {
System.out.println("这是线程C");
}
}
Test22_Thread
packagecom.ygtq.review.basic;
完全可以把线程需要操作的资源类合并到线程类当中来,即将线程需要操作的方法定义到线程类里面,同步块依然写在方法中需要同步的代码上面,这样就不用将资源类传递来传递去了,直接创建线程类对象在启动就可以了
public classTest22_Thread {
public static void main(String[] args){
ThreadA myThread=newThreadA();
//起2个线程,这2个线程使用的是同一个资源对象myThread,已经对需要同步的资源对象进行了同步
Thread thread1=newThread(myThread,"thread1");
Thread thread2=newThread(myThread,"thread2");
thread1.start();
thread2.start();
}
}
//线程1
class ThreadAimplements Runnable{
int count;
String name;
//这是一个进行同步的方法
public void myMethod(){
synchronized (this) {
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i< 5; i++) {
System.out.println(threadName+" "+(count++));
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
//这是一个非同步的方法
public void myMethod2(){
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i < 5;i++) {
System.out.println(threadName+": 这是非同步的方法");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
StringthreadName=Thread.currentThread().getName();
if ("thread1".equals(threadName)){
myMethod();
}else if("thread2".equals(threadName)) {
myMethod2();
}
}
}
class ThreadCextends Thread{
public void run() {
System.out.println("这是线程C");
}
}
Test23_Thread
packagecom.ygtq.review.basic;
/*
* 注意理解:所谓线程同步的本质思想就是:对于某个类中的某个方法,在同一时刻只能被一个线程所调用,这个类的方法中的各种对象、属性称为资源,这种资源是只能进行单一访问的,即是需同步的资源;即在操作这个方法或者方法里面的某些代码时必须进行同步。实现同步的思路有两种:①可以在这个需同步的类的方法中的指定代码上面加上同步块,即在资源区加上同步块,表明这些操作就是要同步的,当某个线程进入这个同步块时,就给同步块里面涉及到的对象加上对象锁,其他线程不能再同时执行这个代码区了;②也可以在线程上面加同步块,即加在run()函数里面,对于线程中去操作可能出现同步安全问题的代码,在上面加上同步块。由于创建线程类后会有很多线程对象,这些线程对象操作时相同的,例如取款线程,那么对其加上同步块后表明,对于创建的多个线程thread1,thread2 thread3等线程,一旦有某个线程执行了同步块中的语句,那么对于语句块中操作的哪些资源对象就会加上对象锁,即远方的资源已经被我这个线程占领了,其他线程不能再占用。其实这2中同步机制是一样的,只是同步块加的位置不一样。总结:对于同一个线程类下的多个线程实例去操作不同资源类中的方法,此时适合在线程类run()函数中加同步块,较为方便;对于多个不同线程类下的线程实例操作同一个资源类中方法中的某些代码语句,适合在资源类中加同步块;其实都可以,就看哪个方便. 注意:无论同步代码块加在资源类里面还是线程run函数里面,只有当操作同一个资源对象时才会进行同步,如果操作的是不同的对象是不会同步的,因为对象锁总是加在某个对象上的,除非synchronized加在类上或者加在静态方法上面。千万注意:synchronized(date) 中的对象可以指定也可以随意写一个对象,但是这个对象一定要是已经存在的,不能是刚刚newDate()的对象.
*/
public classTest23_Thread {
public static void main(String[] args){
ThreadA myThread=newThreadA();
//起2个线程,这2个线程使用的是同一个资源对象myThread,已经对需要同步的资源对象进行了同步
Thread thread1=newThread(myThread,"thread1");
Thread thread2=new Thread(myThread,"thread2");
Thread thread3=newThread(myThread,"thread3");
thread1.start();
thread2.start();
thread3.start();
}
}
//线程1
class ThreadAimplements Runnable{
int count;
String name;
//这是一个进行同步的方法
public void myMethod(){
String threadName=Thread.currentThread().getName();
for (int i = 0; i< 5; i++) {
System.out.println(threadName+" "+(count++));
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
//这是一个非同步的方法
public void myMethod2(){
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i < 5;i++) {
System.out.println(threadName+": 这是非同步的方法");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
private byte[] lock=new byte[0];
public void run() {
synchronized (lock) {
myMethod();
}
}
}
Test24_Thread
packagecom.ygtq.review.basic;
/*
* 使用synchronized来修饰一个方法Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized voidmethod()synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。使用synchronized方法和synchronized代码块其实是一样的,synchronized代码块也是放在方法体里面,只是它并不要求对方法体内的全部代码进行同步而只要求部分同步,而synchronized方法是对该方法内部的全部相关对象进行同步。对于Runnable中的run方法,也可以加上synchronized来进行修饰。在使用synchronized修饰方法时需要注意几点:
①synchronized关键字不能继承:因此,即使父类某方法使用synchronized进行了同步,子类中该方法默认是没有同步的,可以手动重写该方法或者通过super()调用父类中的该方法②在接口方法中不能使用synchronized关键字(接口用来实现,反正synchronized也不能传递到实现类中去)③构造方法不能使用synchronized关键字修饰方法,但是可以使用synchronized同步块
*/
public classTest24_Thread {
public static void main(String[] args){
ThreadA myThread=newThreadA();
//起2个线程,这2个线程使用的是同一个资源对象myThread,已经对需要同步的资源对象进行了同步
Thread thread1=newThread(myThread,"thread1");
Thread thread2=newThread(myThread,"thread2");
Thread thread3=newThread(myThread,"thread3");
thread1.start();
thread2.start();
thread3.start();
}
}
//线程1
class ThreadAimplements Runnable{
int count;
String name;
//这是一个进行同步的方法
public void myMethod(){
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i< 5; i++) {
System.out.println(threadName+" "+(count++));
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
//这是一个非同步的方法
public void myMethod2(){
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i < 5;i++) {
System.out.println(threadName+": 这是非同步的方法");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
//对于Runnable中的run方法也可以使用synchronized来对其进行同步
public synchronized void run() {
myMethod();
}
}
Test25_Thread
packagecom.ygtq.review.basic;
importjava.util.Date;
/**
*使用Synchronized修饰静态方法理解:所谓同步其实是对一个方法中的某些代码使用synchronized等机制进行同步保护,而这个代码必然存在于某个类中,这个包含同步代码的类就是资源类,我们要调用同步代码块中的资源代码必然是通过创建资源类的对象来调用的,即在线程中总是通过资源类的对象来调用同步代码,因此线程同步的原理是当有线程使用某个资源对象来调用代码块时,就锁定这个资源对象,从而保证其他线程不能获取同一个资源对象,从而不能使用同一个资源类对象调用相同代码块,从而起到保护作用。
①使用synchronized修饰非静态方法或者使用synchronized代码块时,同步机制锁定的是资源类对象,因此只有当多个线程同时访问同一个资源类对象的同步代码块时才会锁定对象进行同步,而如果多个线程访问的是不同对象中的同步代码块(即2个线程中调用同步代码块的对象不是同一个对象),那么将会无法进行同步
②相对的,如果使用synchronized对静态方法或者类进行修饰,那么此时锁定的是类本身,即对所有属于资源类的对象进行锁定;此时多个线程中只要有一个线程通过一个资源类的对象调用了同步块或者同步方法,那么其他线程都无法同时调用这个类的同步语句,即不管是通过同一个资源类对象还是不同的资源类对象,都无法进行同步访问。
*/
public classTest25_Thread {
public static void main(String[] args){
ThreadA myThread1=newThreadA();
ThreadA myThread2=newThreadA();
//起2个线程,这2个线程使用的是不同的资源类对象
Thread thread1=newThread(myThread1,"thread1");
Thread thread2=newThread(myThread2,"thread2");
thread1.start();
thread2.start();
}
}
//线程1
class ThreadAimplements Runnable{
static int count;
String name;
//这是一个进行同步的方法,使用synchronized修饰静态方法public synchronized static void myMethod()
public void myMethod(){
//或者使用同步类,可以是任意类
synchronized (Date.class) {
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i< 5; i++) {
System.out.println(threadName+" "+(count++));
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
//这是一个非同步的方法
public void myMethod2(){
StringthreadName=Thread.currentThread().getName();
for (int i = 0; i < 5;i++) {
System.out.println(threadName+": 这是非同步的方法");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
//对于Runnable中的run方法也可以使用synchronized来对其进行同步
public void run() {
myMethod();
}
}
res.wait()和res.notify()
1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
2)调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁,或者叫管程)
3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;
4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;
在Java中,是没有类似于PV操作、进程互斥等相关的方法的。JAVA的进程同步是通过synchronized()来实现的,需要说明的是,Java的synchronized()方法类似于操作系统概念中的互斥内存块,在Java中的Object类对象中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现Java中简单的同步、互斥操作。明白这个原理,就能理解为什么synchronized(this)与synchronized(static XXX)的区别了,synchronized就是针对内存区块申请内存锁,this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例。如果需要在线程间相互唤醒就需要借助Object类的wait()方法及nofity()方法。
说了这么一堆,可能似懂非懂,那么接下来用一个例子来说明问题,用多线程实现连续的1,2,1,2,1,2,1,2,1,2输出。
1. class NumberPrint implements Runnable{
2. private int number;
3. public byte res[];
4. public static int count = 5;
5. public NumberPrint(int number, byte a[]){
6. this.number = number;
7. res = a;
8. }
9. public void run(){
10. synchronized (res){
11. while(count-- > 0){
12. try {
13. res.notify();//唤醒等待res资源的线程,把锁交给线程(该同步锁执行完毕自动释放锁)
14. System.out.println(" "+number);
15. res.wait();//释放CPU控制权,释放res的锁,本线程阻塞,等待被唤醒。
16. System.out.println("------线程"+Thread.currentThread().getName()+"获得锁,wait()后的代码继续运行:"+number);
17. } catch (InterruptedException e) {
18. // TODO Auto-generated catch block
19. e.printStackTrace();
20. }
21. }//end of while
22. return;
23. }//synchronized
24.
25. }
26. }
27. public class WaitNotify {
28. public static void main(String args[]){
29. final byte a[] = {0};//以该对象为共享资源
30. new Thread(new NumberPrint((1),a),"1").start();
31. new Thread(new NumberPrint((2),a),"2").start();
32. }
33. }
输出结果:
1. 1
2. 2
3. ------线程1获得锁,wait()后的代码继续运行:1
4. 1
5. ------线程2获得锁,wait()后的代码继续运行:2
6. 2
7. ------线程1获得锁,wait()后的代码继续运行:1
8. 1
9. ------线程2获得锁,wait()后的代码继续运行:2
下面解释为什么会出现这样的结果:
首先1、2号线程启动,这里假设1号线程先运行run方法获得资源(实际上是不确定的),获得对象a的锁,进入while循环(用于控制输出几轮):
1、此时对象调用它的唤醒方法notify(),意思是这个同步块执行完后它要释放锁,把锁交给等待a资源的线程;
2、输出1;
3、该对象执行等待方法,意思是此时此刻起拥有这个对象锁的线程(也就是这里的1号线程)释放CPU控制权,释放锁,并且线程进入阻塞状态,后面的代码暂时不执行,因未执行完同步块,所以1也没起作用;
4、在这之前的某时刻线程2运行run方法,但苦于没有获得a对象的锁,所以无法继续运行,但3步骤之后,它获得了a的锁,此时执行a的唤醒方法notify(),同理,意思是这个同步块执行完后它要释放锁,把锁交给等待a资源的线程;
5、输出2;
6、执行a的等待方法,意思是此时此刻起拥有这个对象锁的线程(也就是这里的2号线程)释放CPU控制权,释放锁,并且线程进入阻塞状态,后面的代码暂时不执行,因未执行完同步块,所以2号线程的4步骤的唤醒方法也没起作用;
7、此时1号线程执行到3步骤,发现对象锁没有被使用,所以继续执行3步骤中wait方法后面的代码,于是输出:------线程1获得锁,wait()后的代码继续运行:1;
8、此时while循环满足条件,继续执行,所以,再执行1号线程的唤醒方法,意思是这个同步块执行完后它要释放锁;
9、输出1;
10、执行等待方法,线程1阻塞,释放资源锁;
11、此时线程2又获得了锁,执行到步骤6,继续执行wait方法后面的代码,所以输出:------线程2获得锁,wait()后的代码继续运行:2;
12、继续执行while循环,输出2;
··· ···
通过上述步骤,相信大家已经明白这两个方法的使用了,但该程序还存在一个问题,当while循环不满足条件时,肯定会有线程还在等待资源,所以主线程一直不会终止。当然这个程序的目的仅仅为了给大家演示这两个方法怎么用。
总结:
wait()方法与notify()必须要与synchronized(resource)一起使用。也就是wait与notify针对已经获取了resource锁的线程进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait()线程在获取对象锁后,主动释放CPU控制权,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的释放操作。【因此,我们可以发现,wait和notify方法均可释放对象的锁,但wait同时释放CPU控制权,即它后面的代码停止执行,线程进入阻塞状态,而notify方法不立刻释放CPU控制权,而是在相应的synchronized(){}语句块执行结束,再自动释放锁。】释放锁后,JVM会在等待resoure的线程中选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制,而在同步块中的Thread.sleep()方法并不释放锁,仅释放CPU控制权。
Test26_Thread.java
packagecom.ygtq.review.basic;
import java.util.Date;
/**
* 对于java线程同步中的notify()、notifyAll()和wait()的理解:
* 注意:notify()、notifyAll()和wait()都是对象object本身的方法而不是线程Thread提供的方法;
* notify()和wait()是在已经实现synchronized的基础上进行进一步控制使用的。
* notify()和wait()的作用是:对于同一个资源对象res,已经使用synchronized对其进行了同步,于是在某一时刻,只有一个线程可以访问同步代码块中的对象,当线程A执行完代码块后,线程B再执行关于指定资源对象的同步代码块;即此时每个线程需要完全执行完同步代码块后才能进行切换;现在提出一种需求,需要使用两个线程交替执行同一个代码块(由于任意时刻只能有一个线程执行,且必须严格交替,因此只能使用同一个资源对象,且只能在同一个代码块执行期间对2个线程进行交替执行,即必须手动的暂停线程A,启动线程B;暂停线程B,启动线程A)于是此时需要使用wait、notify机制。
* 注意:wait、notify只对单一资源对象的同步起作用,当多线程使用的是不同的资源对象时,wait、notify无法起到对线程交替执行的作用
* 注意:wait、notify必然使用在synchronized代码块的里面,且先使用notify再使用wait方法
* 注意:res.wait()和res.notify()在调用时的对象是资源对象,即是synchronized加上对象锁的那个资源对象;理论上是使用资源对象,但是也可以使用任意对象但是必须保证在synchronized(res)中的对象和res.wait(),res.notify()中的对象是相同的;否则会出现java.lang.IllegalMonitorStateException违法的监控状态异常;即当某个线程试图等待wait自己并不拥有的对象(O)的监控器或者通知其他线程等待该对象(O)的监控器时,抛出该异常
* 注意:Thread.sleep()与Object.wait()的区别在于:wait之后线程阻塞同时object释放对象锁,但是sleep之后线程阻塞却不释放对象锁于是其他线程也不能访问资源对象,必须等到sleep休眠时间结束后线程自动苏醒。
*/
public class Test26_Thread {
public static voidmain(String[] args) {
ThreadA myThread1= new ThreadA();
// 起2个线程,这2个线程使用的是不同的资源类对象
Thread thread1 =new Thread(myThread1, "thread1");
Thread thread2 =new Thread(myThread1, "thread2");
thread1.start();
thread2.start();
}
}
//线程1
class ThreadA implements Runnable{
static int count;
String name;
Integer integer=newInteger(0);
//对于Runnable中的run方法也可以使用synchronized来对其进行同步
public void run() {
// 或者使用同步类,可以是任意类
synchronized(integer) {
StringthreadName = Thread.currentThread().getName();
for (inti = 0; i < 5; i++) {
System.out.println(threadName+ " " + (count++));
try{
/*notify用来唤醒其他正在等待的线程,wait仅表示暂停当前线程,当一个线程A,wait后,如果有 其他非wait状态的
线程B,他们会立即执行,但如果其他是wait状态的线程,不会自动苏醒,要想让wait状态的线程 可以执行,必须在线程Await
之前使用nitify()或者notifyAll()来唤醒其他wait状态的线程。因此,notify()和wait()总是先后出现, 用在一起。*/
integer.notify();
integer.wait();
Thread.sleep(1000);
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
Java并发编程:Lock
在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问。本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock。也许有朋友会问,既然都可以通过synchronized来实现同步访问了,那么为什么还需要提供Lock?这个问题将在下面进行阐述。本文先从synchronized的缺陷讲起,然后再讲述java.util.concurrent.locks包下常用的有哪些类和接口,最后讨论以下一些关于锁的概念方面的东西
以下是本文目录大纲:
一.synchronized的缺陷
二.java.util.concurrent.locks包下常用的类
三.锁的相关概念介绍
若有不正之处请多多谅解,并欢迎批评指正。
请尊重作者劳动成果,转载请标明原文链接:
一.synchronized的缺陷
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
二.java.util.concurrent.locks包下常用的类
下面我们就来探讨一下java.util.concurrent.locks包中常用的类和接口。
下面来逐个讲述Lock接口中每个方法的使用,lock()、tryLock()、tryLock(longtime, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition()这个方法暂且不在此讲述,会在后面的线程协作一文中讲述。
在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?
首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
2.ReentrantLock
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。
例子1,lock()的正确使用方法
输出结果:
Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁
例子3,lockInterruptibly()响应中断的使用方法:
运行之后,发现thread2能够被正确中断。
3.ReadWriteLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
4.ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。下面通过几个例子来看一下ReentrantReadWriteLock具体用法。假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果:
这段程序的输出结果会是,直到thread1执行完读操作之后,才会打印thread2执行读操作的信息。
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0读操作完毕
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
而改成用读写锁的话:
此时打印的结果为:
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
说明thread1和thread2在同时进行读操作。
这样就大大提升了读操作的效率。
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
关于ReentrantReadWriteLock类中的其他方法感兴趣的朋友可以自行查阅API文档。
5.Lock和synchronized的选择
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
三.锁的相关概念介绍
在前面介绍了Lock的基本使用,这一节来介绍一下与锁相关的几个概念。
1.可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下面这段代码就明白了:
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
2.可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。
3.公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
看一下这2个类的源代码就清楚了:
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:
ReentrantLock lock = new ReentrantLock(true);
如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
另外在ReentrantLock类中定义了很多方法,比如:
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁
在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。
4.读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。
上面已经演示过了读写锁的使用方法,在此不再赘述。
如何终止java线程
在java多线程编程中,线程的终止可以说是一个必然会遇到的操作。但是这样一个常见的操作其实并不是一个能够轻而易举实现的操作,而且在某些场景下情况会变得更复杂更棘手。
Java标准API中的Thread类提供了stop方法可以终止线程,但是很遗憾,这种方法不建议使用,原因是这种方式终止线程中断临界区代码执行,并会释放线程之前获取的监控器锁,这样势必引起某些对象状态的不一致(因为临界区代码一般是原子的,不会被干扰的),具体原因可以参考资料[1]。这样一来,就必须根据线程的特点使用不同的替代方案以终止线程。根据停止线程时线程执行状态的不同有如下停止线程的方法。
1 处于运行状态的线程停止
处于运行状态的线程就是常见的处于一个循环中不断执行业务流程的线程,这样的线程需要通过设置停止变量的方式,在每次循环开始处判断变量是否改变为停止,以达到停止线程的目的,比如如下代码框架:
如果主线程调用该线程对象的stop方法,blinker对象被设置为null,则线程的下次循环中blinker!=thisThread,因而可以退出循环,并退出run方法而使线程结束。将引用变量blinker的类型前加上volatile关键字的目的是防止编译器对该变量存取时的优化,这种优化主要是缓存对变量的修改,这将使其他线程不会立刻看到修改后的blinker值,从而影响退出。此外,Java标准保证被volatile修饰的变量的读写都是原子的。
上述的Thread类型的blinker完全可以由更为简单的boolean类型变量代替。
2 即将或正在处于非运行态的线程停止
线程的非运行状态常见的有如下两种情况:
可中断等待:线程调用了sleep或wait方法,这些方法可抛出InterruptedException;
Io阻塞:线程调用了IO的read操作或者socket的accept操作,处于阻塞状态。
2.1 处于可中断等待线程的停止
如果线程调用了可中断等待方法,正处于等待状态,则可以通过调用Thread的interrupt方法让等待方法抛出InterruptedException异常,然后在循环外截获并处理异常,这样便跳出了线程run方法中的循环,以使线程顺利结束。
上述的stop方法中需要做的修改就是在设置停止变量之后调用interrupt方法:
特别的,Thread对象的interrupt方法会设置线程的interruptedFlag,所以我们可以通过判断Thread对象的isInterrupted方法的返回值来判断是否应该继续run方法内的循环,从而代替线程中的volatile停止变量。这时的上述run方法的代码框架就变为如下:
需要注意的是Thread对象的isInterrupted不会清除interrupted标记,但是Thread对象的interrupted方法(与interrupt方法区别)会清除该标记。
2.2 处于IO阻塞状态线程的停止
Java中的输入输出流并没有类似于Interrupt的机制,但是Java的InterruptableChanel接口提供了这样的机制,任何实现了InterruptableChanel接口的类的IO阻塞都是可中断的,中断时抛出ClosedByInterruptedException,也是由Thread对象调用Interrupt方法完成中断调用。IO中断后将关闭通道。以文件IO为例,构造一个可中断的文件输入流的代码如下:
实现InterruptableChanel接口的类包括FileChannel,ServerSocketChannel,SocketChannel, Pipe.SinkChannel andPipe.SourceChannel,也就是说,原则上可以实现文件、Socket、管道的可中断IO阻塞操作。
虽然解除IO阻塞的方法还可以直接调用IO对象的Close方法,这也会抛出IO异常。但是InterruptableChanel机制能够使处于IO阻塞的线程能够有一个和处于中断等待的线程一致的线程停止方案。
3 处于大数据IO读写中的线程停止
处于大数据IO读写中的线程实际上处于运行状态,而不是等待或阻塞状态,因此上面的interrupt机制不适用。线程处于IO读写中可以看成是线程运行中的一种特例。停止这样的线程的办法是强行close掉io输入输出流对象,使其抛出异常,进而使线程停止。最好的建议是将大数据的IO读写操作放在循环中进行,这样可以在每次循环中都有线程停止的时机,这也就将问题转化为如何停止正在运行中的线程的问题了。
有时,线程中的run方法需要足够健壮以支持在线程实际运行前终止线程的情况。即在Thread创建后,到Thread的start方法调用前这段时间,调用自定义的stop方法也要奏效。从上述的停止处于等待状态线程的代码示例中,stop方法并不能终止运行前的线程,因为在Thread的start方法被调用前,调用interrupt方法并不会将Thread对象的中断状态置位,这样当run方法执行时,currentThread的isInterrupted方法返回false,线程将继续执行下去。
为了解决这个问题,不得不自己再额外创建一个volatile标志量,并将其加入run方法的最开头:
还有一种解决方法,也可以在run中直接使用该自定义标志量,而不使用isInterrupted方法判断线程是否应该停止。这种方法的run代码框架实际上和停止运行时线程的一样。