Java多线程--同步和死锁
一、同步问题的出现
通过Runnable接口实现多线程,类中的属性被多个对象共享。此时就出现了访问冲突这个严重的问题。
典型的java例子:
class myThread implements Runnable{
private int ticket = 5;
public void run(){
for(int i = 0;i < 50;i++){
if(ticket > 0){
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName());
System.out.println(" Ticket:" + ticket--);
}
}
}
}
public class SyncDemo{
public static void main(String[]args){
myThread my = new myThread();
Thread t1 = new Thread(my,"Runnable-A");
Thread t2 = new Thread(my,"Runnable-B");
Thread t3 = new Thread(my,"Runnable-C");
t1.start();
t2.start();
t3.start();
}
}
在上例中通过现实Runnable接口实现多线程,产生了三个线程对象,三个线程对象都是通过Runnale接口子类myThread实例化。三个线程对象共享ticket属性。这个程序的运行结果:
Runnable-B Ticket:5
Runnable-A Ticket:4
Runnable-C Ticket:3
Runnable-B Ticket:2
Runnable-A Ticket:1
Runnable-C Ticket:0
Runnable-B Ticket:-1
分析运行结果发现,出现了票数为0 和-1的情况,那么一个线程就有可能在还没有对票数进行减操作之前,其他线程已经将票数减少了,这样出现了票数为非正的情况。如果解决这样的问题,必须使用同步。同步就是指多个操作在一个时间段内只能有一个线程进行操作,其他线程要等待该线程操作完之后再进行操作。
二、同步
解决资源共享的同步操作,可以使用同步代码块和同步方法两种方式完成。
Java中的每一个对象都有一个锁(lock),或者叫监视器(monitor),还称为同步互斥锁。当一个线程访问某个对象的synchronized方法时,将该对象上锁,其他线程都无法访问该对象的synchronized方法了,直到之前的那个线程执行完毕或者抛出异常,才会将该对象的锁释放掉。
1、同步代码块
同步代码块的格式:
synchronized(同步对象){
需要同步的代码;
}
使用同步代码块的时候必须指定一个需要同步的对象,但是一般设置成当前对象(this)。使用的最多的一种方式。
class myThread implements Runnable{
private int ticket = 5;
public void run(){
for(int i = 0;i < 50;i++){
synchronized(this){
if(ticket > 0){
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName());
System.out.println(" Ticket:" + ticket--);
}
}
}
}
}
public class SyncDemo{
public static void main(String[]args){
myThread my = new myThread();
Thread t1 = new Thread(my,"Runnable-A");
Thread t2 = new Thread(my,"Runnable-B");
Thread t3 = new Thread(my,"Runnable-C");
t1.start();
t2.start();
t3.start();
}
}
2、同步方法
使用synchronized关键字将一个方法修成同步方法。相当于第一种方式的缩写。
同步方法的格式:
访问权限 synchronized 方法返回值 方法名称(参数列表){
方法体;
}
使用同步方法示例:
class myThread implements Runnable{
private int ticket = 5;
public void run(){
for(int i = 0;i < 50;i++){
this.sale();
}
}
public synchronized void sale(){
if(this.ticket > 0){
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName());
System.out.println(" Ticket:" + this.ticket--);
}
}
}
public class SyncDemo{
public static void main(String[]args){
myThread my = new myThread();
Thread t1 = new Thread(my,"Runnable-A");
Thread t2 = new Thread(my,"Runnable-B");
Thread t3 = new Thread(my,"Runnable-C");
t1.start();
t2.start();
t3.start();
}
}
使用synchronized关键的注意事项:
(1) 只能同步方法和代码块,而不能同步变量和类。
(2) 每个对象只有一个同步锁。当使用同步时,要弄清在哪个对象上同步。如果是其他一个无关的对象,就没用了。
(3) 不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
(4) 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
(5) 如果线程有同步和非同步方法,非同步方法可以被多个线程自由访问而不受限制。
(6) 线程睡眠时,它所持的任何同步锁都不会释放。
(7) 线程可以获得多个同步锁。
(8) 同步损害并发性,应该尽量缩小使用范围。能用同步代码块就用同步代码块。
(9) 编写线程安全的代码会使系统总体性能降低,要适量使用。
一个线程取得了同步锁什么时候会释放掉:
(1) 同步代码块或者同步方法正常结束
(2) 使用return或break终止执行,或者抛出未处理的异常。
(3) 当线程执行同步代码块或方法时,程序执行了同步锁对象的wait()方法。
三、死锁
死锁:多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程无限期的阻塞,导致程序无法正常运行。
死锁的根源:过多和不适当的运synchronized关键词来管理线程对特定的对象访问。
因此避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源A、B、C时,保证使每个线程都按照同样的顺序去访问它们,比如都先访问A,在访问B和C。