Java之多线程

1 线程的概念

多线程,就类似于操作系统的多进程。可以同时并发执行多个任务,处理多件事情。

线程是一个轻量级的进程,一个进程中可以分为多个线程。一个进程中至少需要有一个线程。

相对于进程,线程所使用的系统资源更少,切换更加容易。

2 实现多线程的方法

1 继承Thread

Threadjava中的线程类,可以通过继承Thread类来实现多线程的操作。

继承Thread类,重写其中的run方法,run方法中的代码就是线程所要执行的任务。通过调用对象的start方法即可启动线程。

注意:要启动线程,需要调用对象的start方法,而不是它的run方法。调用run方法无法实现多线程的效果。

Main方法就是一个线程。这个线程的名字就叫main

自己编写的线程类,如果没有使用setName方法设置线程的名字,jvm会将它们命名为thread-0thread-1...

下面是一个以便洗澡,一边听音乐的示例,该示例使用继承Thread类的方法实现多线程:

package cn.ancony.thread;

public class ThreadTest {
    public static void main(String[] args) {
        MissionA a = new MissionA();
        MissionB b = new MissionB();
        a.start();
        b.start();
    }
}

class MissionA extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName() + " 洗澡中" + (i + 1) + "/" + 20 + "...");
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MissionB extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName() + " 听音乐中" + (i + 1) + "/" + 20 + "...");
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

程序运行结果如下:

Thread-0 洗澡中1/20...

Thread-1 听音乐中1/20...

Thread-1 听音乐中2/20...

Thread-0 洗澡中2/20...

Thread-1 听音乐中3/20...

Thread-0 洗澡中3/20...

Thread-0 洗澡中4/20...

Thread-1 听音乐中4/20...

Thread-1 听音乐中5/20...

Thread-0 洗澡中5/20...

Thread-1 听音乐中6/20...

Thread-0 洗澡中6/20...

Thread-1 听音乐中7/20...

Thread-0 洗澡中7/20...

Thread-0 洗澡中8/20...

Thread-1 听音乐中8/20...

Thread-1 听音乐中9/20...

Thread-0 洗澡中9/20...

Thread-1 听音乐中10/20...

Thread-0 洗澡中10/20...

Thread-0 洗澡中11/20...

Thread-1 听音乐中11/20...

Thread-1 听音乐中12/20...

Thread-0 洗澡中12/20...

Thread-1 听音乐中13/20...

Thread-0 洗澡中13/20...

Thread-1 听音乐中14/20...

Thread-0 洗澡中14/20...

Thread-1 听音乐中15/20...

Thread-0 洗澡中15/20...

Thread-0 洗澡中16/20...

Thread-1 听音乐中16/20...

Thread-0 洗澡中17/20...

Thread-1 听音乐中17/20...

Thread-0 洗澡中18/20...

Thread-1 听音乐中18/20...

Thread-0 洗澡中19/20...

Thread-1 听音乐中19/20...

Thread-0 洗澡中20/20...

Thread-1 听音乐中20/20...

2实现Runnable接口

步骤:

1创建一个类,实现Runnable接口。Runnable接口是一个函数式接口,实现的时候,可以直接使用Lambda表达式。接口中只有一个run方法。我们需要重写该方法。

2创建该类的对象,将对象作为Thread的运行目标。

package cn.ancony.thread;

public class RunnableTest {
    public static void main(String[] args) {
        //第一种写法,将同一个对象传到两个线程中。
//        MissionC c = new MissionC();
//        Thread t1 = new Thread(c);
//        Thread t2 = new Thread(c);

        //第二种写法,两个线程分别new一个对象。
        
Thread t1 = new Thread(new MissionC());
        Thread t2 = new Thread(new MissionC());
        t1.start();
        t2.start();
    }
}

class MissionC implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("听音乐中" + (i + 1) + "/" + 10 + "...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

使用Thread类和实现Runnable接口的区别:

A当一个类继承了其他的类,而这个类又要实现多线程的时候,就需要实现Runnable接口,而不能再继承Thread类,因为Java是单继承的。

B如果想在实现多线程的类里面使用一些Thread类里面的方法,比如getName()sleep()等方法的时候,使用Thread类就可以使用这些方法,而不像实现Runnable接口。在实现Runnable接口的类中使用上述的方法时,需要使用Thread类的静态方法方法获得一个Thread的实例,然后通过该实例调用相应的方法,此时,直接使用Thread类可以减少程序的繁琐。

C当实现不同不同的任务时,在上面的例子中,通过第一种方式实现多线程,可以实现一边洗澡一边听音乐,而通过第二种实现,那就是一边音乐,一边还是听音乐。实现不同的任务,使用Thread类会更好。

D当需要共享变量的时候,使用实现Runnable接口的方式会更好。

3线程的生命周期

A新建--创建对象

B就绪--调用对象的start方法以后。

C运行--获得CPU的时间片

D阻塞(挂起)--失去CPU的时间片

E死亡--线程执行结束或者抛出未捕获的异常。

4 Thread类的常用方法

1 public static Thread currentThread();

获得当前的线程,任何类都可以直接使用Thread.currentThread这个静态方法获得当前的线程对象。

2 public String getName() 获得线程的名称,实例方法。

3 public final native boolean isAlive();判断线程是否存活。

在线程的start方法调用之后,在线程死亡之前,调用该方法返回true

注意:在创建完线程对象,而没有调用start方法的时候,调用该方法返回的是false

4 public void join()等待直到这个线程死亡。

如果在A线程中调用了B线程的join方法,则A线程会等到B线程执行结束,A线程才会继续执行。

join方法还有两个重载的方法:

public void join(long millis);

public void join(long millis,int nanos)

带参数的join方法会至多等待参数指定的时间。join方法很可能提前结束,而不会滞后。不带参数的join方法会无限等待。

5 public static void sleep(long millis);使当前的线程睡眠(暂时停止执行)millis毫秒。如果当前程序存在其他等待的线程,则其他线程会获得执行机会。该方法会使当前的线程阻塞。同时该方法如果在同步块中,不会释放已经获得的锁。调用了该方法,该线程就一定会让出自己的时间片。当参数的时间到达后,该线程会重新转为就绪状态。但就绪后,不意味着马上会得到执行。线程在sleep期间,如果其他线程调用了该线程的interrupt方法,则该线程就会中断,并产生InterruptException异常。

6 public static void yield();

当前运行的线程有意让出CPU资源,由线程调度器重新选择线程调度。不过,这仅仅是一个提示,线程调度器可能会忽略。

7 public void setDaemon(boolean on);

设置线程是否为后台线程。

只能在线程对象的start方法调用之前使用该方法。如果在调用了start方法之后使用,会抛出IllegalThreadStateException异常。

注意:当没有前台线程执行的时候,jvm就会退出。程序就会结束。jvm的垃圾回收器是一个典型的后台进程。

8 public void setPriority(int newPriority);

设置线程的优先级。优先级为1-10。优先级的数字越大,优先级越高。优先级高的线程仅仅意味这可能获得更多的执行机会,不表示一定会一直执行,优先级低的线程仍然有机会执行。

通常使用高中低三个优先级就够了。

Thread类中高中低优先级的定义是:

 public final static int MIN_PRIORITY = 1;
 
public final static int NORM_PRIORITY = 5;
 
public final static int MAX_PRIORITY = 10;

5线程同步

当多线程并发运行时,多线程间很可能操作共享成员变量,此时,就需要对共享成员变量的操作进行同步,避免出现多线程的并发修改而造成的意外错误。

同步的代码在同一时刻,至多只有一个线程执行,使用线程锁机制来保证。

线程同步的实现方式:

1使用同步块

因为线程的同步涉及到操作共享变量,所以使用实现Runnable接口的方式来实现多线程。

同步块的写法:

synchronized () {

共享变量的访问

}

任意的对象都可以充当锁对象。对于同步块,一般使用this

下面是三个人抢100 张火车票的例子。

package cn.ancony.thread;

public class TicketTest {
    public static void main(String[] args){
        Ticket ticket=new Ticket();
        Thread t1=new Thread(ticket);
        Thread t2=new Thread(ticket);
        Thread t3=new Thread(ticket);
        t1.setName("张三");
        t2.setName("李四");
        t3.setName("王五");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket implements Runnable{
private int ticket=100;
    @Override
    public void run() {
        while(ticket>0){
            synchronized (this){
                System.out.println(Thread.currentThread().getName()+"抢到了第"+ticket+"张票。");
                ticket--;
            }
        }
    }
}

这个时候运行结果很不尽人意,每次运行的结果都不一致。很可能一个人抢完了所有的票,也有可能一个人一直抢了几十张票。所以我们让线程sleep一下。

那么问题来了,sleep方法写在哪里呢?

一个是可以写在同步块里面,一个写在同步块外面。应该写在同步块外面。(为什么?)

更改后的代码如下:

@Override
public void run() {
    while (ticket > 0) {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票。");
            ticket--;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

完成后运行,这段代码有的时候能正常运行,有的时候不能正常运行。主要出在最后一张票的时候。当三个线程有可能同时进入了while循环之后,同步块之前,这个时候,ticketvi1。但是三个线程已经全部执行完了while(ticket>0)这条语句。所以按顺序执行后面的语句,就出现了0-1的情况。

程序需要进一步修改。那需要把ticke>0这个条件也放入同步块中。

但是像下面写可以吗?

@Override
public void run() {
    synchronized (this) {
        while (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票。");
            ticket--;
        }
    }
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

其实是不可以的,这完全是一个人抢完了所有的票,根本没有多线程的效果。

那怎么办呢?

@Override
public void run() {
    while (true) {
        synchronized (this) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票。");
                ticket--;
            } else {
                break;
            }
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2使用同步方法

使用同步方法进行同步,整个方法都是同步区域。对于实例方法,this会充当同步方法的锁。对于静态方法,类的Class对象会充当锁。

class Method {
    public synchronized void instanceMethod1() {
    }

    public synchronized void instanceMethod2() {
    }

    public static synchronized void staticMethod1() {
    }

    public static synchronized void staticMethod2() {
    }
}

在上面的类中,有四个同步方法,两个实例同步方法和两个静态同步方法。

instanceMethod1instanceMethod2方法是互斥的。当一个线程在操作同步方法instanceMethod1时,其他的线程自然不能操作instanceMethod1,但是也不能操作instanceMethod2,因为instanceMethod1instanceMethod1使用的是同一把锁!它们的锁都是当前的this对象。

staticMethod1staticMethod2方法是互斥的。原理同上面一样。不能同时访问是因为它们的锁一样,都是这个类的Class对象。

3 synchronizedLock

JDK1.5之前,同步都是使用synchronized关键字来实现的,就像上面的例子一样。在JDK1.5之后,通过Lock接口来实现。该接口中有一个lock方法用来加锁,还有一个unlock方法用来解锁。

Lock的使用方式也很简单。将synchronized语句块中的所有语句,使用lock.lock()lock.unlock包围即可。

代码如下:

class Ticket implements Runnable {
    private int ticket = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票。");
                ticket--;
            } else {
                break;
            }
            lock.unlock();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

但是这个时候其实还是有问题的。程序输出结果完成后,还会一直处于阻塞的状态。

这是为什么呢?因为使用lock来实现加锁的时候,如果加锁的语句块中出现了未捕获的异常或者使用了break之后,这个锁是释放不掉的。而使用synchronized则没有这个问题。

synchronized语句块中,如果出现了未捕获的异常和使用了break语句之后,锁会自动释放掉。那如果还想使用lock的方式来同步怎么办呢?我们知道try...finally语句中,finally中的语句不管是有没有异常都会得到执行的。所以,我们可以将同步块的语句使用try...finally来包围。代码如下:

class Ticket implements Runnable {
    private int ticket = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票。");
                    ticket--;
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

所以,我们在使用lock的时候,往往会配合try...finally来实现。

4 Interrupt

线程中断异常InterruptedException

如果一个线程在sleep,join,wait方法休眠或等待过程中,其他线程调用了该线程的interrupt方法来中断该线程,则会产生InterruptedException异常。

如果该线程没有处于sleep,join,wait休眠或等待中,则不会产生该异常。

代码如下:

package cn.ancony.thread;

public class InterruptTest {
    public static void main(String[] args){
        ThreadA a=new ThreadA();
        a.start();
        a.interrupt();
    }
}
class ThreadA extends Thread{
    @Override
    public void run() {
        try {
            sleep(5000);
        } catch (InterruptedException e) {
            System.out.println();
            e.printStackTrace();
        }
    }
}

像上面的代码,可以保证一定可以产生InterruptException异常吗?

不是的。如果在main方法中,已经执行完了a.interrupt()这句代码,而线程对象a还没有执行,就不能保证产生这个异常。为了确保产生这个异常,我们可以让main线程也sleep,保证main线程比线程对象a这个线程先醒来即可。

修改以后的代码如下:

package cn.ancony.thread;

public class InterruptTest {
    public static void main(String[] args) {
        ThreadA a = new ThreadA();
        a.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a.interrupt();
    }
}

class ThreadA extends Thread {
    @Override
    public void run() {
        try {
            sleep(5000);
        } catch (InterruptedException e) {
            System.out.println("线程被中断。");
            e.printStackTrace();
        }
    }
}

6 死锁

当两个或者多个线程同时拥有自己的资源,而相互等待获得对方资源,导致程序永远陷入僵持的状态,这就是死锁。

但多线程并发访问共享数据的时候,使用同步操作可以避免多线程并发修改带来的危害,但是同时也有可能会产生死锁。

一个典型的打架的例子。这个例子就会产生死锁。

package cn.ancony.test;

public class DeadLock {
    public static void main(String[] args){
        Fighter a=new Fighter("张三");
        Fighter b=new Fighter("李四");
        Thread t1=new Thread(()->a.hold(b));
        Thread t2=new Thread(()->b.hold(a));
        t1.start();
        t2.start();
    }
}
class Fighter{
    private String name;

    public Fighter(String name) {
        this.name = name;
    }
    public synchronized void hold(Fighter f){
        System.out.println(name+"抓住了"+f.name+",等待对方先松手。");
        f.loose(this);
        loose(f);
    }
    public synchronized void loose(Fighter f){
        System.out.println(name+"松开了"+f.name);
    }
}

7等待与唤醒

在多线程通信时,在某些特定条件下,我们需要线程做出一定的让步,否则就很容易造成双方(或者多方)进行僵持的状态,进而造成死锁。

A线程运行一个同步方法的时候,发现需要B线程的一个执行结果,这个时候,A线程就应该主动让出自己的锁,等待B线程执行完成之后再执行。

那么怎么去实现呢?使用sleep方法可以吗?这个时候使用sleep方法是不合适的。Sleep方法一定会让出自己的时间片,CPU会去执行其他的任务,但是sleep方法并不会让出自己的锁。而且,也不能保证线程苏醒后,条件就一定会得到满足。这个时候我们应该使用wait方法。Wait会令当前线程等待,直到另一个线程调用该线程的notify或者notifyAll方法。当前线程必须要拥有该对象的锁。当调用wait方法后,线程会释放掉其占有的锁,并处于等待队列中。Wait方法是Object里面的方法,所以,所有的类都有这个方法。

那么A怎么知道B线程已经执行完了呢?这个时候最好的策略就是B线程执行完了以后通知A。使用notify或者notifyAll方法来实现。Notify唤醒等待该对象锁的一个进程,如果有多个线程处于等待中,仅唤醒一个。具体哪一个,取决于底层的实现。notifyAll会唤醒等待该对象锁的所有线程。这两个方法也是Object中声明的,所有的类都有这个方法。

注意:wait,notifynotifyAll方法调用时,当前线程一定要拥有对象的锁,否则将会引起IllegalMonitorStateException异常。

Sleep方法和wait方法有什么区别呢?

1sleep不会放弃已经占有的锁,而wait会。

2. SleepThread线程里面的方法,只有Thread类及其子类才有这个方法。而wait方法,所有的java类中都有这个方法。

NotitynotifyAll方法的区别:

当调用了wait方法的时候,当前的线程就会让出自己所占有的锁资源,同时会处于等待之中,当前线程就会加入一个阻塞队列。Notity方法是从阻塞队列中随机通知一个线程,具体通知的哪个线程,并不确定。所以如果同时有多个线程在阻塞队列中,并不能确定到底会通知谁。而notifyAll是通知处于阻塞队列中的所有线程。每个线程收到通知后,会检查是不是符合自己的条件,如果符合自己的条件,那么它就会从阻塞的状态变为就绪的状态。

 

一个经典的等待和唤醒的例子是生产者和消费者的例子。在这个例子中,有一个公共的仓库,用于存放物品。生产者生产了产品以后,把产品放在仓库中。消费者去仓库中消费产品。生产者生产物品和消费者消费物品都是同步的方法。当生产者生产的速度大于消费者消费的速度时,物品就会堆满仓库。这个时候,生产者就不能再生产了,需要消费者给它创造生产的条件(消费者要把物品消费掉,腾出仓库的空间,这样生产者才可以继续生产),生产者就应该等着消费者去消费(如果消费者没有消费,就应该去通知消费者去消费)。相反也是,当消费者消费的速度大时,仓库就会变空,这时候,消费者就不能再消费了,需要等待生产者创造消费的条件(生产物品,使仓库不为空)。这时,消费者就等待生产者生产物品(如果生产者没有生产,那么就应该通知生产者生产)。

package cn.ancony.thread;

import java.util.ArrayList;
import java.util.List;

public class ProductAndConsum {
    public static void main(String[] rags){
        Worker worker=new Worker();
        Thread a=new Thread(()->{
            for(int i=0;i<100;i++){
                worker.produce();
            }
        });
        Thread b=new Thread(()->{
            for(int i=0;i<100;i++){
                worker.consume();
            }
        });
        a.start();
        b.start();
    }
}
class Worker{
    private List<String> list=new ArrayList<>();
    public synchronized void produce(){
        if(list.size()==3){
            System.out.println("仓库已满,生产者阻塞");
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            list.add("production");
            notifyAll();
        }
    }
    public synchronized void consume(){
        if(list.size()==0){
            System.out.println("仓库已空,消费者阻塞");
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            list.remove("production");
            notifyAll();
        }
    }
}

以上是JDK1.5之前的方法。在JDK1.5之后,还可以使用LockCondition来实现。

代码如下:

package cn.ancony.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProductAndConsum {
    public static void main(String[] rags) {
        Worker worker = new Worker();
        Thread a = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                worker.produce();
            }
        });
        Thread b = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                worker.consume();
            }
        });
        a.start();
        b.start();
    }
}

class Worker {
    private List<String> list = new ArrayList<>();
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void produce() {
        try {
            lock.lock();
            if (list.size() == 3) {
                System.out.println("仓库已满,生产阻塞");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                list.add("production");
                condition.signalAll();
            }
        } finally {
            lock.unlock();
        }
    }

    public void consume() {
        try {
            lock.lock();
            if (list.size() == 0) {
                System.out.println("仓库已空,消费阻塞");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                list.remove("production");
                condition.signalAll();
            }
        } finally {
            lock.unlock();
        }
    }
}

 

 

猜你喜欢

转载自blog.csdn.net/ancony_/article/details/80288138
今日推荐