【Java之】多线程学习笔记

时间:2023-02-25 15:19:48

最近在学习thinking in java(第三版),本文是多线程这一章的学习总结。

 -----------------------------------------------------------------------------------------------------------------------------------------------------------

内容:

  1. 创建线程的两种方法,继承Thread和implements Runnable接口。

  2. 设置线程的优先权Priorities。

  3. 后台线程。

  4. 线程的join方法。

  5. 介绍两种匿名内部类的实现。

  6. 有响应的用户界面实例。

  7. 不正确的访问资源AlwaysEven.java。

  8. 解决资源共享问题的方法:synchronized。1,同步方法。2,同步方法块。DualSynch.java

  9. 线程阻塞的四个原因。

  10. 线程时间的协作,wait()和notify(),notifyAll(),厨师和服务员的例子。

------------------------------------------------------正文开始--------------------------------------------------------------------- 

 

1,创建线程的两种方法。

  • 继承Thread类:

  代码:

public class SimpleThread extends Thread {

    private int countDown = 5;

    private static int threadCount = 0;

    public SimpleThread(){

        super("" + ++threadCount );//参数是线程名

        start();

    }

    public String toString(){

        return "#" + getName() + " : " + countDown;

    }

    public void run(){

        while(true){

            System.out.println(this);

            if(--countDown == 0) return;

        }

    }
    public static void main(String[] args) throws InterruptedException {

        // TODO Auto-generated method stub

        for(int i = 0; i < 5; i++){

            new SimpleThread();
        }
    }
}

通过getName()得到线程名,System.out.println(this);时this返回的是toString()返回的字符串。

整个步骤:在main函数中,首先调用构造器来构造对象,在构造器中调用了start()方法来配置线程,然后由线程执行机制调用run()。如果不调用start(),线程永远不会启动。(start也可以不在构造器中调用。)

注:在main()中创建若干个Thread对象的时候,并没有获得他们中的任何一个的引用。对于普通对象而言这会使它成为垃圾回收器要回收的目标,但对于Thread对象就不会了。每个Thread对象都需要“注册”自己,所以实际上在某个地方存在着对它的引用,垃圾回收器只有在线程离开了run()并且死亡之后才能把它清理掉。

 

  • implements Runnable接口:

如果你的类已经继承了其他的类,在这种情况下,就不可能同时还继承Thread(Java不支持多重继承,但是支持多实现)。这时可以实现Runnable接口来达到目的。

代码:

public class RunnableThread implements Runnable {

    private int countDown = 5;
    public String toString(){

        return "#" + Thread.currentThread().getName() + " : " + countDown;

    }
    public void run(){

        while(true){

            System.out.println(this);

            if(--countDown == 0) return;

        }
    }
    public static void main(String[] args) throws InterruptedException {

        // TODO Auto-generated method stub

        for(int i = 1; i <= 5; i++){

            new Thread(new RunnableThread(),""+i).start();

        }
    }
}

 

由于不是继承Thread类,所以调用getName()需要通过调用Thread.currentThread()方法。

 

 

2,设置线程的优先权Priorities:

优先权告诉调度程序线程的重要性如何。如果有许多线程被阻塞,那么调度程序将倾向于让优先权最高的线程先执行。

代码:

public class SimplePriorities extends Thread {
    private int countDown = 5;
//  private volatile double d = 0;
    public SimplePriorities(int priority){
        setPriority(priority);
        start();
    }
    public String toString(){
        return super.toString() + " : " + countDown;
    }
    public void run(){
        while(true){
            System.out.println(this);
            if(--countDown == 0) return;
        }
    }
    public static void main(String[] args){
        // TODO Auto-generated method stub
        new SimplePriorities(Thread.MAX_PRIORITY);
        for(int i = 0; i < 5; i++){
            new SimplePriorities(Thread.MIN_PRIORITY);
        }
    }
}

运行结果:

Thread[Thread-0,10,main] : 5
Thread[Thread-0,10,main] : 4
Thread[Thread-0,10,main] : 3
Thread[Thread-0,10,main] : 2
Thread[Thread-0,10,main] : 1
Thread[Thread-2,1,main] : 5
Thread[Thread-4,1,main] : 5
Thread[Thread-2,1,main] : 4
Thread[Thread-5,1,main] : 5
Thread[Thread-5,1,main] : 4
Thread[Thread-1,1,main] : 5
Thread[Thread-4,1,main] : 4
Thread[Thread-4,1,main] : 3
Thread[Thread-1,1,main] : 4
Thread[Thread-5,1,main] : 3
Thread[Thread-5,1,main] : 2
Thread[Thread-5,1,main] : 1
Thread[Thread-2,1,main] : 3
Thread[Thread-1,1,main] : 3
Thread[Thread-1,1,main] : 2
Thread[Thread-1,1,main] : 1
Thread[Thread-4,1,main] : 2
Thread[Thread-3,1,main] : 5
Thread[Thread-3,1,main] : 4
Thread[Thread-4,1,main] : 1
Thread[Thread-2,1,main] : 2
Thread[Thread-3,1,main] : 3
Thread[Thread-2,1,main] : 1
Thread[Thread-3,1,main] : 2
Thread[Thread-3,1,main] : 1

 

在上面代码中,使用super.toString()用来打印线程的名称,优先级,线程所属的‘线程组’。

可以自己指定优先级别(1-10),不过为了可移植性,一般使用MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5),主线程的优先级默认是5。

 

 

3,后台线程:

后台线程又称守护线程,只有所有非后台线程结束,程序才终止,也就是说,后台线程是伴随着非后台线程的,非后台线程不存在了,自然后台线程也就结束了。Main()就是一个非后台线程。

代码:

public class SimpleDaemons extends Thread{

    public SimpleDaemons() {
        // TODO Auto-generated constructor stub
        setDaemon(true);
        start();
    }
    @Override

    public void run() {

        // TODO Auto-generated method stub
        while(true){
            try {
                sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(this+"---"+this.isDaemon());
//          return;
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        for(int i = 0; i < 10; i++)
            new SimpleDaemons();
    }
}

 

必须在start()前调用setDaemon()方法,才能把它设置为后台线程。

上述代码执行后没有结果,因为当主线程结束,程序就终止了,后台线程在要打印之前就已经结束了。如果把setDaemon()注释掉,再执行的结果完全不一样,将是无限循环的打印出线程信息,因为当主线程结束后,在主线程中创建的非后台线程还没有结束,程序并没有终止。

 

 

4,线程的join方法。

一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。Join()方法也可以带上一个超时参数(毫秒),这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。

我觉得join()方法就是线程的插队嘛…

代码:

class MySleeper extends Thread{
    public MySleeper(String name){
        super(name);
        start();
    }
    public void run(){
        try {
            sleep(1500);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(getName()+" has awakened.");
        
    }
}

class MyJoiner extends Thread{
    private MySleeper mysleeper;
    
    public MyJoiner(String name, MySleeper mysleeper){
        super(name);
        this.mysleeper = mysleeper;
        start();
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            mysleeper.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(getName()+" join completed.");
    }
    
}

public class MyJoining {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MySleeper
            mysleepy = new MySleeper("Sleepy");
        MyJoiner
            dopey = new MyJoiner("Dopey",mysleepy);
    }
}

运行结果:(1.5s后)
Sleepy has awakened.
Dopey join completed.

 

上面代码中在MyJoiner的run方法中调用了mysleeper.join(),即表明MySleeper线程插入到了MyJoiner线程之前去了,当前者执行完后再执行后者。

 

5,介绍两种匿名内部类的实现:

代码:

class InnerThread {
    //继承自Thread的匿名内部类
    private int countDown = 5;
    private Thread t;
    //构造函数
    public InnerThread(String name){
        t= new Thread(name){
            public void run(){
                while(true){
                    System.out.println(this);
                    if(--countDown == 0) return;
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
            public String toString(){
                return getName()+": "+countDown;
            }
        };
        t.start();
    }
}

class InnerRunnable{
    //实现Runnable接口的匿名内部类
    private int countDown = 5;
    private Thread t;
    //构造函数
    public InnerRunnable(String name){
        t = new Thread(new Runnable(){
            public void run(){
                while(true){
                    System.out.println(this);
                    if(--countDown == 0) return;
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
            public String toString(){
                return Thread.currentThread().getName()+": "+countDown;
            }
        },name);
        t.start();
    }
}

public class Anonymous {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //使用匿名内部类
        new InnerThread("InnerThread");
        new InnerRunnable("InnerRunnable");
    }
}

执行结果:
InnerThread: 5
InnerRunnable: 5
InnerThread: 4
InnerRunnable: 4
InnerThread: 3
InnerRunnable: 3
InnerThread: 2
InnerRunnable: 2
InnerThread: 1
InnerRunnable: 1

 

6,有响应的用户界面实例。

下面的这个例子,是一个利用多线程实现的一边专注于运算,一边得带控制台输入的多线程应用。如果是单线程,将无法做到。

代码:

class UnresponsiveUI {
    private volatile double d = 1;
    public UnresponsiveUI() throws Exception {
        while(d>0){
            d = d +(Math.PI+Math.E) / d;
        }
        System.in.read();
    }
}


public class ResponsiveUI extends Thread{
    private static volatile double d = 1;
    public ResponsiveUI(){
        setDaemon(true);
        start();
    }
    public void run(){
        while(true)
            d = d + (Math.PI+Math.E) / d;
    }

    public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
//        new UnresponsiveUI();
        new ResponsiveUI();
//        Thread.sleep(3000);//主线程休眠
        System.in.read();
        
        System.out.println(d);
    }
}

运行结果:
输入a
打印出
24706.75604567532

 

执行后,任意输入一个数,就会打印出在ResponsiveUI中d的计算结果值。

如果去掉//      new UnresponsiveUI();的注释,上面代码在UnresponsiveUI中将永远无法执行System.in.read();

 

7,不正确的访问资源:

当多个线程使用同一个资源时,会发生资源冲突问题。你永远不会知道线程是何时运行的,想象一下你坐在桌子旁边,手里有一把叉子,准备叉起盘子里的最后一块食物,当叉子碰到食物的时候,它突然消失了(因为你的线程被挂起,另一个线程跑进来偷走了食物)。

下面这个例子可以说明问题:

代码:

public class AlwaysEven {
    private int i;
    public void next(){
        i++;i++;
    }
    public int getValue(){
        return i;
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final AlwaysEven ae = new AlwaysEven();
        new Thread("Watcher"){
            public void run(){
                int val = ae.getValue();
                if(val % 2 != 0){
                    System.out.println(val);
                    System.exit(0);
                }
            }
        }.start();
        while(true){
            ae.next();
        }
    }
}

 

从代码上看返回值显然都会是偶数,但是我的执行结果却是返回了1691.

 

 

8,解决资源共享问题的方法:synchronized。

既然多个线程操作同一个资源会出现意想不到的问题,那么解决办法是什么呢?

答案就是synchronized。

一般来说,共享资源一般是以对象形式存在的内存片断,也有可能是文件等其他形式。总之要控制对共享资源的访问,需要把它包装进一个对象。

每个对象都含有单一的锁,这个锁本身就是对象的一部分。当在对象上调用synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。

除了给synchronized方法外,如果你只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,可以使用同步控制块来实现。形式如下:

Synchronized(syncObject) {
         //code
}

 

在进入此段代码前,必须得到syncObject对象的锁。如果其他线程已经得到这个锁,那么就得等到锁被释放以后才能进入。

 

同步方法和同步代码块代码实例:

public class DualSynch {
    private Object syncObject = new Object();
    public synchronized void f(){
        System.out.println("Inside f()");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            throw new RuntimeException(e);
        }
        System.out.println("Leaving f()");
    }
    public void g(){
        synchronized (syncObject) {
            System.out.println("inside g()");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("Leaving g()");
        }
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final DualSynch ds = new DualSynch();
        new Thread(){
            public void run(){
                ds.f();
            }
        }.start();
        ds.g();
    }
}

运行结果:
inside g()
Inside f()
Leaving f()
(等待约10秒)
Leaving g()

 

注:如果你将g()改成使用synchronized方法而不是synchronized (syncObject)块,你会发现执行结果是先输出

inside g()

过10秒后再输出

Leaving g()
Inside f()
Leaving f()

另外,同步代码块的方式会比同步方法速度快一点,所以宁愿使用同步代码块而不是对整个方法进行同步控制。

9,线程阻塞的四个原因:

  • sleep()
  • wait()会将线程挂起,直到线程得到了notify()或notifyAll(),线程才会进入就绪状态。
  • 线程在等待某个输入/输出。
  • 线程试图在某个对象上调用其同步控制方法,但是对象锁不可用。

10,线程时间的协作,wait()和notify(),notifyAll(),厨师和服务员的例子:

在了解了线程之间可能存在的相互冲突以及怎样避免冲突之后,下一步是学习怎样使线程之间相互协作。这种协作通过Object的方法wait()和notify()来实现的。

注意sleep()和wait()的区别:

在wait()期间的对象锁是释放的,sleep()的时候锁并没有释放。

 

注意只能在同步控制方法或同步控制块里调用wait()、notify()和notifyAll()。

 

厨师与服务员的例子:

考虑某个餐馆,有一个厨师和一个服务员。服务员必须等待厨师准备好食物。当厨师准备好食物的时候,他通知服务员,后者将得到食物,然后回去继续等待。这是一个线程协作的极好的例子:厨师代表了生产者,服务员代表了消费者。

下面是模拟这个场景的代码:

class Order {
    private static int i = 0;
    private int count = i++;
    public Order(){
        if(count == 10){
            System.out.println("Out of food, closing");
            System.exit(0);
        }
    }
    public String toString(){
        return "Order "+count;
    }
}

class Waiter extends Thread{
    private Restaurant restaurant;
    public Waiter(Restaurant r){
        restaurant = r;
        start();
    }
    public void run(){
        while(true){
            while(restaurant.order == null)
                synchronized (this) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            System.out.println("Waiter got "+ restaurant.order);
            restaurant.order = null;
        }
    }
}

class Chef extends Thread{
    private Restaurant restaurant;
    private Waiter waiter;
    public Chef(Restaurant r,Waiter w){
        restaurant = r;
        waiter = w;
        start();
    }
    public void run(){
        while(true){
            if(restaurant.order == null){
                restaurant.order = new Order();
                System.out.println("Order up! ");
                synchronized (waiter) {
                    waiter.notify();
                }
            }
            try {
                sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

public class Restaurant {
    Order order;
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Restaurant restaurant = new Restaurant();
        Waiter waiter = new Waiter(restaurant);
        Chef chef = new Chef(restaurant,waiter);
    }
}

运行结果:
Order up! 
Waiter got Order 0
Order up! 
Waiter got Order 1
Order up! 
Waiter got Order 2
Order up! 
Waiter got Order 3
Order up! 
Waiter got Order 4
Order up! 
Waiter got Order 5
Order up! 
Waiter got Order 6
Order up! 
Waiter got Order 7
Order up! 
Waiter got Order 8
Order up! 
Waiter got Order 9
Out of food, closing

注意对notify()的调用必须首先获取Waiter对象waiter的锁。这样能保证如果两个线程试图在同一个对象上调用notify()时也不会互相冲突。

 

关于线程协作的例子,PipedWriter和PipedReader管道输入和输出是另外一个例子,允许不同的线程分别控制同一个管道的输入和输出,具体代码就不贴上来了。

另外Synchronized也会出现问题:死锁

因为线程可以阻塞,且对象可以具有同步控制方法用以防止别的线程在锁还没有释放的时候就访问这个对象;所以就可能出现这种情况:某个线程在等待另一个线程,而后者又等待别的线程,这样一直下去,直到这个链条上的线程又在等待第一个线程释放锁。这得到了一个相互等待的死循环,没有哪个线程能继续。

经典死锁问题:五个哲学家就餐问题。