java多线程同步和死锁

一、java多线程的实现

     在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口;Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限。

     public class Thread extends Object implements Runnable ,Thread类也是Runnable接口的子类。

     1. 通过继承Thread类实现多线程

public class ThreadTest extends Thread {

    private String threadName;

    public ThreadTest(String threadName) {
        super();
        this.threadName = threadName;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程开始:" + this.threadName + ",i=" + i);
        }
    }
}
public static void main(String[] args) {
        
        ThreadTest t1=new ThreadTest("线程a");  
        ThreadTest t2=new ThreadTest("线程b");  
        t1.start();
        t2.start();

}

输出的结果可能是这样(因为需要用到CPU的资源来分配线程,所以每次的运行结果可能是不一样的):

线程开始:线程a,i=0
线程开始:线程b,i=0
线程开始:线程a,i=1
线程开始:线程b,i=1
线程开始:线程a,i=2
线程开始:线程b,i=2
线程开始:线程a,i=3
线程开始:线程a,i=4
线程开始:线程b,i=3
线程开始:线程b,i=4

注意:虽然我们在这里调用的是start()方法,但是实际上调用的还是run()方法的主体,通过查看源代码代码找到Thread中的start()方法的定义,可以发现此方法中使用了private native void start0();其中native关键字表示可以调用操作系统的底层函数,那么这样的技术成为JNI技术(java Native Interface)

 

     2. 通过实现Runnable接口实现多线程

     在实际开发中一个多线程的操作很少使用Thread类,而是通过Runnable接口完成。

public class ThreadTest implements Runnable {

    private String threadName;

    public ThreadTest(String threadName) {
        super();
        this.threadName = threadName;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程开始:" + this.threadName + ",i=" + i);
        }
    }
}
public static void main(String[] args) {
        
        ThreadTest t1=new ThreadTest("线程a");  
        ThreadTest t2=new ThreadTest("线程b");  
        new Thread(t1).start();
        new Thread(t2).start();

}

可能的输出结果为(因为需要用到CPU的资源来分配线程,所以每次的运行结果可能是不一样的):

线程开始:线程a,i=0
线程开始:线程a,i=1
线程开始:线程a,i=2
线程开始:线程b,i=0
线程开始:线程b,i=1
线程开始:线程b,i=2
线程开始:线程b,i=3
线程开始:线程b,i=4
线程开始:线程a,i=3
线程开始:线程a,i=4

     在使用Runnable定义的子类中没有start()方法,只有Thread类中才有。此时观察Thread类,有一个构造方法:public Thread(Runnable targer)此构造方法接受Runnable的子类实例,也就是说可以通过Thread类来启动Runnable实现的多线程。

 

     3. 两种实现方式的区别和联系

     在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:

     a.避免点继承的局限,一个类可以继承多个接口。 b.适合于资源的共享

public class MyThread extends Thread {
    private int ticket = 10;

    public void run() {
        for (int i = 0; i < 20; i++) {
            if (this.ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "卖票:ticket" + this.ticket--);
            }
        }
    }
}
public static void main(String[] args) {
        
        MyThread t1=new MyThread();  
        MyThread t2=new MyThread(); 
        t1.start();
        t2.start();

}

可能的输出:

Thread-0卖票:ticket10
Thread-1卖票:ticket10
Thread-0卖票:ticket9
Thread-1卖票:ticket9
Thread-0卖票:ticket8
Thread-1卖票:ticket8
Thread-0卖票:ticket7
Thread-1卖票:ticket7
Thread-0卖票:ticket6
Thread-1卖票:ticket6
Thread-0卖票:ticket5
Thread-1卖票:ticket5
Thread-0卖票:ticket4
Thread-1卖票:ticket4
Thread-0卖票:ticket3
Thread-1卖票:ticket3
Thread-0卖票:ticket2
Thread-1卖票:ticket2
Thread-0卖票:ticket1
Thread-1卖票:ticket1

说明:两个线程,每个线程都在卖自己的票,总共10张票,结果却卖了20张,这就没有达到资源的共享。

注意:在Thread中就不可以用同一个实例化对象t1,否则会报错。

t1.start();
t1.start();//会报错

如果用Runnable就可以实现资源共享,下面看例子:

package Others;

public class MyThread implements Runnable {
    private int ticket = 10;

    public void run() {
        for (int i = 0; i < 20; i++) {
            if (this.ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "卖票:ticket" + this.ticket--);
            }
        }
    }
}
public static void main(String[] args) {
        
        MyThread t1=new MyThread();  
        new Thread(t1).start();
        new Thread(t1).start();

}

输出:

Thread-0卖票:ticket10
Thread-1卖票:ticket9
Thread-0卖票:ticket8
Thread-1卖票:ticket7
Thread-0卖票:ticket6
Thread-1卖票:ticket5
Thread-0卖票:ticket4
Thread-1卖票:ticket3
Thread-0卖票:ticket2
Thread-1卖票:ticket1

说明:虽然现在程序中有2个线程,但是一共卖了10张票,也就是说使用Runnable实现多线程可以达到资源共享目的。

但是在正常环境中,卖票的网络可能会出现延迟,例如我们将代码修改成这样:

public class MyThread implements Runnable {
    private int ticket = 10;

    public void run() {
        
            for (int i = 0; i < 20; i++) {
                if (this.ticket > 0) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "卖票:ticket" + this.ticket--);
                }
            }
        
    }
}

可能输出的结果就成了:

Thread-1卖票:ticket10
Thread-0卖票:ticket9
Thread-0卖票:ticket8
Thread-1卖票:ticket8
Thread-1卖票:ticket7
Thread-0卖票:ticket6
Thread-1卖票:ticket5
Thread-0卖票:ticket4
Thread-0卖票:ticket3
Thread-1卖票:ticket3
Thread-0卖票:ticket2
Thread-1卖票:ticket2
Thread-0卖票:ticket1
Thread-1卖票:ticket0

说明:因为一个线程就有可能在还没有对票数进行操作没有结束的时候,其他线程就已经获取当前票数了。

要解决这样的问题,就要使用同步,同步指的是在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成之后才能继续进行。

修改代码如下:

public class MyThread implements Runnable {
    private int ticket = 10;

    public void run() {
        synchronized (this) {
            for (int i = 0; i < 20; i++) {
                if (this.ticket > 0) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "卖票:ticket" + this.ticket--);
                }
            }
        }
    }
}
public static void main(String[] args) {
        
        MyThread t1=new MyThread();  
        new Thread(t1).start();
        new Thread(t1).start();

}

输出:

Thread-0卖票:ticket10
Thread-0卖票:ticket9
Thread-0卖票:ticket8
Thread-0卖票:ticket7
Thread-0卖票:ticket6
Thread-0卖票:ticket5
Thread-0卖票:ticket4
Thread-0卖票:ticket3
Thread-0卖票:ticket2
Thread-0卖票:ticket1

还可以使用同步方法来提高效率:

public class MyThread implements Runnable {
    private int ticket = 10;

    public void run() {
        sale();
    }
    
    public synchronized void sale(){
        for (int i = 0; i < 20; i++) {
            if (this.ticket > 0) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "卖票:ticket" + this.ticket--);
            }
        }
    }
}

 

main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实习在就是在操作系统中启动了一个进程。

 

二、线程的优先级和线程让步

      线程的让步是通过Thread. yield()来实现的。yield()方法的作用是:暂停当前正在执行的线程对象,并执行其他线程。要理解yield(),必须了解线程的优先级的概念。线程总是存在优先级,优先级范围在1~10之间。JVM线程调度程序是基于优先级的抢先调度机制。在大多数情况下,当前运行的线程优先级将大于或等于线程池中任何线程的优先级。但这仅仅是大多数情况。
      注意:当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。
 
      当线程池中线程都具有相同的优先级,调度程序的JVM实现自由选择它喜欢的线程。这时候调度程序的操作有两种可能:一是选择一个线程运行,直到它阻塞或者运行完成为止。二是时间分片,为池内的每个线程提供均等的运行机会。
 
      设置线程的优先级:线程默认的优先级是创建它的执行线程的优先级。可以通过setPriority(int newPriority)更改线程的优先级。例如:
        Thread t = new MyThread();
        t.setPriority(8);
        t.start();
      线程优先级为1~10之间的正整数,JVM从不会改变一个线程的优先级。然而,1~10之间的值是没有保证的。一些JVM可能不能识别10个不同的值,而将这些优先级进行每两个或多个合并,变成少于10个的优先级,则两个或多个优先级的线程可能被映射为一个优先级。
 线程默认优先级是5,Thread类中有三个常量,定义线程优先级范围:
static int MAX_PRIORITY 
          线程可以具有的最高优先级。 
static int MIN_PRIORITY 
          线程可以具有的最低优先级。 
static int NORM_PRIORITY 
          分配给线程的默认优先级。
 
1、Thread.yield()方法
     Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
 
2、join()方法
     Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。例如:
     Thread t = new MyThread();
     t.start();
     t.join();
 
小结,线程离开运行状态的3种方法:
1、调用Thread.sleep():使当前线程睡眠至少多少毫秒(尽管它可能在指定的时间之前被中断)。
2、调用Thread.yield():不能保障太多事情,尽管通常它会让当前运行线程回到可运行性状态,使得有相同优先级的线程有机会执行。
3、调用join()方法:保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。
 除了以上三种方式外,还有下面几种特殊情况可能使线程离开运行状态:
1、线程的run()方法完成。
2、在对象上调用wait()方法(不是在线程上调用)。
3、线程不能在对象上获得锁定,它正试图运行该对象的方法代码。
4、线程调度程序可以决定将当前运行状态移动到可运行状态,以便让另一个线程获得运行机会,而不需要任何理由。
 
 
三、线程的同步和死锁
     线程发生死锁可能性很小,即使看似可能发生死锁的代码,在运行时发生死锁的可能性也是小之又小。
     发生死锁的原因一般是两个对象的锁相互等待造成的。
     Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。  
     导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。 
   下面是一个死锁的小例子:
public class TestDeadLock implements Runnable
{
    public int flag = 1;
    static Object o1= new Object(); 
    static Object o2 = new Object();

    /**
     * @param args
     */
    public static void main(String[] args)
    {
        TestDeadLock td1 = new TestDeadLock();
        TestDeadLock td2 = new TestDeadLock();
        td1.flag=1;
        td2.flag=0;
        Thread t1 = new Thread(td1);
        Thread t2 = new Thread(td2);
        t1.start();
        t2.start();
    }

    public void run()
    {
        if(flag == 1){
            synchronized(o1){
                try
                {
                    Thread.sleep(500);
                }
                catch (InterruptedException e)
                {
                }
                synchronized(o2){
                    System.out.println("1");
                }
            }
        }
        if(flag==0){
            synchronized(o2){
                try
                {
                    Thread.sleep(500);
                }
                catch (InterruptedException e){
                }
                synchronized(o1){
                    System.out.println("0");
                }
            }
        }
    }

}

说明:t1线程等待t2线程,t2线程又等待t1线程,这样互相等待就造成了多线程的死锁。


1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。
 

部分内容资料引自:http://lavasoft.blog.51cto.com/62575/27069/

转载于:https://www.cnblogs.com/NicholasLee/archive/2012/07/09/2582265.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值