当使用多线程访问一个资源时,非常容易出现线程安全的问题(例如,当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失)。因此需要采用同步机制来解决这种问题。Java主要提供了3种实现同步机制的方法:
1>synchronized关键字
在Java语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一段synchronized代码时,需要先获取这个锁,然后去执行相应的代码,执行结束后,释放锁。
synchronized关键字主要有两种用法(synchronized方法和symchronized块),此外该关键字还可以作用于静态方法、类或某个实例,但这都对程序的效率有很大的影响。
1)synchronized方法。在方法的声明前加入synchronized关键字,示例如下:
public synchronized void multiThreadAccess();
只要把多个线程对类需要被同步的资源的操作放到multiThreadAccess()方法中,就能保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全性。然而,当一个方法的方法体规模非常大时,把该方法声明为synchronized会大大影响程序的执行效率。为了提高程序的效率,Java提供了synchronized块。
2)synchronized块。synchronized块既可以把任意的代码声明为synchronized,也可以指定上锁的对象,有非常高的灵活性。用法如下:
synchronized(synObject){
//访问synObject的代码
}
2>wait()方法与notify()方法
当使用synchronized来修饰某个共享资源时,若线程A1在执行synchronized代码,另一个线程A2也要同时执行同一个对象的同一synchronized代码时,线程A2将要等到线程A1执行完成之后,才能继续执行。在这种情况下可以使用wait()方法和nofity()方法。
在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并且可以调用notify()方法或notifyAll()方法通知正在等待的其他进程。notify()方法仅唤醒一个线程(等待队列中的第一个线程)并允许它去获得锁,notifyAll()方法唤醒所有等待这个对象的线程并允许他们去获得锁(并不是让所有唤醒线程都获取到锁,而是让他们去竞争)。
3>Lock
JDK 5新增了Lock接口以及它的一个实现类RecontrantLock(重入锁),Lock也可以用来实现多线程额同步,具体而言,它提供了如下的一些方法老实现多线程的同步:
1)lock()。以阻塞的方式获取锁,也就是说,如果获取到了锁,立即返回;如果别的栈持有锁,当前线程等待,直到获取锁后返回。
2)tryLock()。以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回true,否则,立即返回false。
3)trylok(long timeout,TimeUnit unit)。如果获取了锁,立即返回true,否则会等待参数给定的时间单元,在等到的过程中,如果获取了锁,就返回true,如果等待超时,返回false。
4)lockInterruptibly()。如果获取了锁,立即返回;如果没有获取锁,当前线程处于休眠状态,直到获得锁,或者当前线程被别的线程中断(会受到InterruptedException异常)。它与lock()方法最大的区别在于如果lock()方法获取不到锁,会一直处于阻塞状态,且会忽略interrupt()方法,示例如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException{
final Lock lock = new ReentrantLock();
lock.lock();
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
try{
lock.lockInterruptibly();
//lock.lock(); //如果只有lock()方法,由于lock()会忽略interrupt()方法,所以不会返回中断信号,所以编译器会报错
}catch(InterruptedException e){
System.out.println("Interrupted.");
}
}
});
t1.start();
t1.interrupt();
Thread.sleep(1);
}
}
运行结果:
Interrupted.
sleep()是使线程暂停执行一段时间的方法。wait()也是一种线程暂停执行的方法,例如,当线程进行交互时,如果线程对一个同步对象x发出一个wait()调用请求,那么该线程就会暂停执行,被调对象进入等待状态,直到被唤醒或等待时间超时。
具体而言,sleep()方法与wait()方法的区别主要表现在以下几个方面:
*1)原理不同。*sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程就会自动”苏醒”,例如,当线程执行报时功能时,每一秒钟打印出一个时间,那么此时就需要在打印方法前面加上一个sleep()方法,以便让自己每隔一秒执行一次,该过程如同闹钟一样。而wait()方法是Object类的方法,用于线程间的通信,这个方法会使当前拥有该对对象锁的进程等待,直到其他进程调用notify()方法(或notifyAll()方法)时才醒来,不过开发人员也可以给它制定一个时间,自动醒来。与wait()方法配套的还有notify()方法和notifyAll()方法。
2)对锁的处理机制不同。由于sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用sleep()方法并不会释放锁。而wait()方法则不同,当调用wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。
3)使用区域不同。由于wait()方法的特殊含义,因此它必须放在同步控制方法或者同步语句块中使用,而sleep()方法则可以放在任何地方使用。
sleep()方法必须捕获异常,而wait()、notify()以及notifyAll()不需要捕获异常。因为在sleep()的过程中,有可能被其他对象调用它的interrupt(),产生InterruptedException异常。
由于sleep不会被释放”锁标志”,容易导致死锁问题的发生。因此一般情况下,不推荐使用sleep()方法,而推荐使用wait()方法。
引申:
1.sleep()方法与yield()方法有什么区别?
答:主要表现在以下几个方面方面:
1>sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,而yield()方法只会给相同优先级或更高优先级的线程以运行的机会。
2>线程执行sleep()方法后会转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内肯定不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后又马上被执行。
3>sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常。
4>sleep()方法比yield()方法(跟操作系统相关)具有更好的可移植性。
2.利用Thread.wait()同步线程,可以设置超时时间吗?
答:可以设置超时,函数原型为wait(long timeout)和wait(long timeout,int nanos),timeout代表最长的等待时间,单位为ms;nanos代表额外的等待时间,单位为ns。
在Java语言中,可以使用stop()方法与suppose()方法来终止线程的执行。当用Thread.stop()来终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,其他线程将会”看”到这个不一致的状态,这可能导致程序执行的不确定性,并且这种问题很难被定位。调用suspend()方法容易发生死锁(死锁指的是两个或者两个以上的进程在执行过程中,因抢夺资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进)。由于调用suspend()方法不会释放锁,这就会导致一个问题:如果用一个suspend挂起一个有锁的进程,那么在锁恢复之前将不会被释放。如果调用suspend()方法,线程将试图取得相同的锁,程序就会发生死锁,例如,线程A已经获取到了互斥资源M的锁,此时线程A通过suspend()方法挂起线程A的执行,接着线程B也去访问互斥资源M,这时候就造成了死锁。鉴于以上两种方法的不安全性,Java语言已经不建议使用以上两种方法来终止线程了。
那么,如何才能终止线程呢?一般建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,即执行完run()方法,也就是说,如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束run()方法的执行。在实现时,可以通过设置一个flag标志来控制循环是否执行,通过这种方法来让线程离开run()方法从而终止线程。夏利给出了结束线程的方法:
public class MyThread implements Runnable{
private volatile Boolean flag;
public void stop(){
flag = false;
}
public void run(){
while(flag)
;//do something
}
}
上例中,通过调用MyThread的stop()方法虽然能够终止线程,答同样也存在问题:当线程处于非运行状态时(当sleep()方法被调用或当wait()方法被调用或当被I/O阻塞时),上面介绍的方法就不可用了。此时可以使用interrupt()方法来打破阻塞的情况,当interrupt()方法被调用时,会抛出InterruptedException异常,可以通过在run()方法中捕获这个异常来让线程安全退出,具体实现方式如下:
public class MyThread{
public static void main(String[] args){
Thread thread = new Thread(new Runnable(){
public void run(){
System.out.println("thread go to sleep");
try{
//用休眠来模拟线程被阻塞
Thread.sleep(5000);
System.out.println("thread finish");
}catch(InterruptedException e){
System.out.println("thread is interrupt");
}
}
});
thread.start();
thread.interrupt();
}
}
运行结果:
thread go to sleep
thread is interrupt
如果程序因为I/O而停滞,进入非运行状态,基本上要等到I/O完成才能离开这个状态,在这种情况下,无法使用interrupt()方法来使程序离开run()方法。这就需要使用一个替代的方法,基本思路也是触发一个异常,而这个异常与所使用的I/O有关,例如,如果使用readLine()方法等待网络上的一个信息,此时线程处于阻塞状态。让程序离开run()方法就是使用close()方法来关闭流,在这种情况下会引发IOException异常,run()方法可以通过捕获这个异常来安全地结束线程。
Java语言提供了两种锁机制来实现对某个共享资源的同步:synchronized和Lock。其中,synchronized使用object对象本身的notify、wait、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,完成synchronized实现的所有功能。
具体而言,二者的主要区别主要表现在以下几个方面的内容:
1>用法不一样。在需要同步的对象中加入synchronized控制,synchronized既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。而Lock需要显式的指定起始位置和终止位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现的,它有比synchronized更精确的线程语义。
2>性能不一样。在JDK 5中增加了一个Lock接口的实现类ReentrantLock。它不仅拥有和synchronized相同的并发性和内存语义,还多了锁投票、定时锁、等候和中断锁等。它们的性能在不同的情况下会有不同;在资源竞争不是很激烈的情况下,synchronized的性能要优于ReentrantLock,但是在资源竞争很激烈的情况下,synchronized的性能会下降的很快,而ReentrantLock的性能基本保持不变。
3>锁机制不一样。synchronized获得锁和释放的方式都是在块结构中,当获得多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁。而Lock则需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题的发生。此外,Lock还提供了更强大的功能,它的tryLock()方法可以采用非阻塞的方式去获取锁。
虽然synchronized与Lock都可以用来实现多线程的同步,但是,最好不要同时使用这两种同步机制,因为ReentranrLock与synchronized所使用的机制不同,所以它们的运行是独立的,相当于两种类型的锁,在使用时互不影响,示例1为使用两种同步方式,代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class SyncTest{
private int value = 0;
Lock lock = new ReentrantLock();
public synchronized void addValueSync(){
this.value++;
System.out.println(Thread.currentThread().getName()+":"+value);
}
public void addValueLock(){
try{
lock.lock();
value++;
System.out.println(Thread.currentThread().getName()+":"+value);
}finally{
lock.unlock();
}
}
}
public class TestSynchronized {
public static void main(String[] args){
final SyncTest st = new SyncTest();
Thread t1 = new Thread(
new Runnable(){
@Override
public void run(){
for(int i=0;i<5;i++){
st.addValueSync();
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
);
Thread t2 = new Thread(
new Runnable(){
@Override
public void run(){
for(int i=0;i<5;i++){
st.addValueLock();
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
);
t1.start();
t2.start();
}
}
运行结果:
Thread-0:2
Thread-1:2
Thread-1:3
Thread-0:4
Thread-0:5
Thread-1:6
Thread-0:7
Thread-1:8
Thread-0:9
Thread-1:10
实例2采用同一种锁的方式,代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class SyncTest{
private int value = 0;
Lock lock = new ReentrantLock();
public synchronized void addValueSync(){
this.value++;
System.out.println(Thread.currentThread().getName()+":"+value);
}
public void addValueLock(){
try{
lock.lock();
value++;
System.out.println(Thread.currentThread().getName()+":"+value);
}finally{
lock.unlock();
}
}
}
public class TestSynchronized {
public static void main(String[] args){
final SyncTest st = new SyncTest();
Thread t1 = new Thread(
new Runnable(){
@Override
public void run(){
for(int i=0;i<5;i++){
st.addValueSync();
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
);
Thread t2 = new Thread(
new Runnable(){
@Override
public void run(){
for(int i=0;i<5;i++){
st.addValueSync();
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
);
t1.start();
t2.start();
}
}
运行结果:
Thread-0:1
Thread-1:2
Thread-1:3
Thread-0:4
Thread-0:5
Thread-1:6
Thread-1:7
Thread-0:8
Thread-1:9
Thread-0:10
实例1中的结果并不是每次运行的结果都是相同的,但是实例2中的结果每次都是相同的。而且发现实例1中的value值并不连续,是因为两种上锁方法采用了不同的机制造成的,因此实际使用时,最好不要同时使用两种上锁机制。
引申:当一个线程进入一个对象的一个synchronized()方法后,其他线程是否可以进入此对象的其他方法?
答案:当一个线程进入一个对象的一个synchronized()方法后,其他线程是否可以进入此对象的其他方法取决于方法本身,如果该方法是非synchronized()方法,那么是可以访问的,示例如下:
class Test{
public synchronized void syncMethod(){
System.out.println("being calling syncMethod");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
System.out.println(e.getMessage());
}
System.out.println("finish calling syncMethod");
}
public void generalMethod(){
System.out.println("calling generalMethos");
}
}
public class MultiThread {
static final Test t = new Test();
public static void main(String[] args){
Thread t1 = new Thread(){
public void run(){
t.syncMethod();
}
};
Thread t2 = new Thread(){
public void run(){
t.generalMethod();
}
};
t1.start();
t2.start();
}
}
运行结果:
being calling syncMethod
calling generalMethos
finish calling syncMethod
从上例可以看出,线程t1在调用syncMethod()方法的过程中,线程t2仍然可以访问同一对象的非syncMethod()方法。
如果其他方法是静态方法(使用static修饰的方法),它用的同步锁是当前类的字节码,与非静态的方法不能同步(因为非静态方法用的是this),因此,静态方法可以被调用,示例如下:
class Test{
public synchronized void syncMethod(){
System.out.println("being calling syncMethod");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
System.out.println(e.getMessage());
}
System.out.println("finish calling syncMethod");
}
public synchronized static void generalMethod(){
System.out.println("calling generalMethos");
}
}
public class MultiThread {
static final Test t = new Test();
public static void main(String[] args){
Thread t1 = new Thread(){
public void run(){
t.syncMethod();
}
};
Thread t2 = new Thread(){
public void run(){
t.generalMethod();
}
};
t1.start();
t2.start();
}
}
运行结果:
being calling syncMethod
calling generalMethos
finish calling syncMethod
从上例中可以看出,当线程t1在调用对象t的synchronized()方法时,线程t2仍然可以调用这个对象的静态synchronized()方法。
Java提供了两种线程:守护线程与用户线程。守护线程又被称为”服务进程” “精灵进程” 或”后台进程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的”保姆”。
用户线程和守护线程几乎一样,唯一不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了, JVM也就退出了。因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会”杀死”所有守护线程。也就是说,只要有任何非守护线程还在运行,程序就不会终止。
在Java语言中,守护线程一般具有较低的优先级,它并非只由JVM提供的,用户在编写程序时也可以自己设置守护线程,例如,将一个用户线程设置为守护线程的方法就是在调用start()方法启动线程之前调用对象的setDaemon(true)方法,若将以上参数设置为false,则表示的是用户进程模式。需要注意的是,当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此。示例如下:
class ThreadDemo extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+":begin");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":end");
}
}
public class Test {
public static void main(String[] args){
System.out.println("test3:begin");
Thread t1 = new ThreadDemo();
t1.setDaemon(true);
t1.start();
System.out.println("test3:end");
}
}
运行结果:
test3:begin
test3:end
Thread-0:begin
从运行结果可以发现,没有输出Thread-0:end。之所以结果是这样,是在启动线程前将其设置为守护线程了,当程序中只有守护线程存在时,JVM是可以退出的,也就是说,当JVM中只有守护线程运行时,JVM会自动关闭。因此,当test3方法调用结束后,main线程将退出,此时线程t1还处于休眠状态没有运行结束,但是由于此时只有守护线程在运行,JVM将关闭,因此不会输出”Thread-0:end”。
守护线程的一个典型例子就是垃圾回收器。只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。
在Java语言中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join()方法后的代码。简单来说,就是将两个线程合并,用于实现同步功能。具体而言,可以通过线程A的join()方法来等待线程A的结束,或者使用线程A的join(2000)方法来等待线程A的结束,但最多等待2秒,示例如下:
class ThreadImp implements Runnable{
public void run(){
try{
System.out.println("Being ThreadImp");
Thread.sleep(5000);
System.out.println("End ThreadImp");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public class TestJoin {
public static void main(String[] args){
Thread t = new Thread(new ThreadImp());
t.start();
try{
t.join(1000);
if(t.isAlive()){
System.out.println("t has not finished");
}else{
System.out.println("t has finished");
}
System.out.println("join finished");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
运行结果:
Being ThreadImp
t has not finished
join finished
End ThreadImp