Java 多线程总结

限于喜欢 提交于 2020-03-28 06:29:36

 昨天熬了个通宵,看了一晚上的视频,把java 的多线程相关技术重新复习了一遍,下面对学习过程中遇到的知识点进行下总结。

 

首先我们先来了解一下进程、线程、并发执行的概念:

 

进程是指:一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。

 

线程是指:进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。

一般来说,当运行一个应用程序的时候,就启动了一个进程,当然有些会启动多个进程。启动进程的时候,操作系统会为进程分配资源,其中最主要的资源是内存空间,因为程序是在内存中运行的。

 

在进程中,有些程序流程块是可以乱序执行的,并且这个代码块可以同时被多次执行。实际上,这样的代码块就是线程体。线程是进程中乱序执行的代码流程。当多个线程同时运行的时候,这样的执行模式成为并发执行。

 

线程的状态

1、线程共有下面4种状态:

 

  • 新建状态(New):新创建了一个线程对象,当你用new创建一个线程时,该线程尚未运行。
  • 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  • 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

a. 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。

b. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM把该线程放入锁。

c. 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  • 死亡状态(Dead):

a. 由于run方法的正常退出而自然死亡;

b. 没有捕获到的异常事件终止了run方法的执行,从而导致线程突然死亡

 

 

2、若要确定某个线程当前是否活着,可以使用 isAlive 方法。

 

如果该线程是可运行线程或者被中断线程,那么该方法返回true;如果该线程仍然是个新建线程,或者该线程是个死线程,那么该方法返回false

 

3、注意:你无法确定一个活线程究竟是处于可运行状态还是被中断状态,也无法确定一个可运行线程是否正处在运行之中。另外,你也无法对尚未成为可运行的线程与已经死掉的线程进行区分。

 

4、线程必须退出中断状态,并且返回到可运行状态,方法是使用与进入中断状态相反的过程:

 

a. 如果线程已经处于睡眠状态,就必须经过规定的毫秒数

b. 如果线程正在等待输入或输出操作完成,那么必须等待该操作完成

c. 如果线程调用了wait方法,那么另外一个线程必须调用notifyAll或者notify方法

d. 如果线程正在等待另一个线程拥有的对象锁,那么另一个线程必须放弃该锁的所有权

 

5、下面这副图很好的反映了线程在不同情况下的状态变化。



了解完多线程的相关知识,下面来介绍一下在java中多线程的实现方式

JAVA多线程实现方式

JAVA多线程实现方式主要有以下三种:

1、继承Thread类

2、实现Runnable接口

3、使用ExecutorService、Callable、Future实现有返回结果的多线程。

 

其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。其中最常用的也是前两种实现方式。下面对前两种实现方式分别做下讲解。

 

1、继承Thread类实现多线程

 

继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。

例如:

package thread;

public class MyThread extends Thread {
    public void run() {
        System.out.println("run()方法正在执行");
    }
}

启动线程方式如下:

MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();

2、实现Runnable接口方式实现多线程

 

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口。

方法如下:

package thread;
class OtherClass{
    public void print(String str){
        System.out.println(str);
    }
}

public class MyThread extends OtherClass implements Runnable {
    public void run() {
        System.out.println("run()正在执行");
    }
}

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例。

具体方法如下:

MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();

事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

public void run() {
    if (target != null) {
        target.run();
    }
}

学会了线程的创建方式,下面我们在举几个线程状态转换的例子.

3、线程状态的转换实例

package thread;

public class ThreadStateDemo extends Thread {

    Thread thread;

    public ThreadStateDemo() {
        thread = new Thread(this);
        System.out.println("创建一个线程:thread");
        thread.start();
    }

    public void run() {
       try {
           System.out.println("线程thread正在运行!");
           System.out.println("线程thread睡眠3秒中...!");
           Thread.sleep(3000);  //静态方法,使当前正在执行的线程睡眠3秒
           System.out.println("线程thread在睡眠后重新运行!");
       }catch(InterruptedException e) {
           System.out.println("线程被中断");
       }
    }

    public static void main(String[] args) {
       new ThreadStateDemo();
       System.out.println("主线程main结束!");
    }
}

【运行结果】如下:

创建一个线程:thread
主线程main结束!
线程thread正在运行!
线程thread睡眠3秒中...!
线程thread在睡眠后重新运行!

终止线程的实例:

package thread;

public class ThreadShutDownDemo {

    public static void main(String args[]) {
        Runner runner = new Runner();
        Thread thread = new Thread(runner);
        thread.start();
        for(int i=0;i<10;i++) {
            if(i%10!=0) {
                System.out.println("在主线程中 i=" + i);
            }
        }
        System.out.println("主线程main结束");
        //通知线程结束
        runner.shutDown();
    }
}

class Runner implements Runnable {
    //控制线程是否结束
    private boolean flag = true;
    
    public void run() {
        int i=0;
        while(flag == true) {
            System.out.println("在子线程中 i=" + i++);
        }
        System.out.println("子线程结束");
    }
    
    //设置线程结束标志
    public void shutDown() {
        flag = false;
    }
}

【运行结果】如下:

在主线程中 i=1
在子线程中 i=0
在主线程中 i=2
在子线程中 i=1
在主线程中 i=3
在子线程中 i=2
在主线程中 i=4
在子线程中 i=3
在主线程中 i=5
在子线程中 i=4
在主线程中 i=6
在主线程中 i=7
在主线程中 i=8
在主线程中 i=9
主线程main结束
在子线程中 i=5
子线程结束

 join()方法实例:

package thread;

public class TheadJoinDemo {

    public static void main(String[] args) {
        Runner2 r = new Runner2();
        Thread t = new Thread(r);
        t.start();
        try {
            t.join();//主线程main将中断,直到线程t执行完毕
        }catch(InterruptedException e) {
        }
        for(int i=0;i<5;i++) {
            System.out.println("主线程:" + i);
        }
    }
}

class Runner2 implements Runnable {
    public void run() {
        for(int i=0;i<10;i++) {
            System.out.println("子线程:" + i);
        }
    }
}

【运行结果】如下:

子线程:0
子线程:1
子线程:2
子线程:3
子线程:4
子线程:5
子线程:6
子线程:7
子线程:8
子线程:9
主线程:0
主线程:1
主线程:2
主线程:3
主线程:4

介绍完以上几个实例,我们下面对sleep()、wait()、yeid()、join()几个方法进行下区别总结

sleep方法与wait方法的区别:

  1. sleep方法是静态方法,wait方法是非静态方法。
  2. sleep方法在时间到后会自己“醒来”,但wait不能,必须由其它线程通过notify(All)方法让它“醒来”。
  3. sleep方法通常用在不需要等待资源情况下的阻塞,像等待线程、数据库连接的情况一般用wait。

sleep/wait与yeld方法的区别:

调用sleep或wait方法后,线程即进入block状态,而调用yeld方法后,线程进入runnable状态。

 

wait与join方法的区别:

  1. wait方法体现了线程之间的互斥关系,而join方法体现了线程之间的同步关系。
  2. wait方法必须由其它线程来解锁,而join方法不需要,只要被等待线程执行完毕,当前线程自动变为就绪。
  3. join方法的一个用途就是让子线程在完成业务逻辑执行之前,主线程一直等待直到所有子线程执行完毕。

线程的同步问题

在实际应用中,我们通常会遇到多线程安全问题。多线程安全问题:当多条语句在操作同一线程共享数据是,一个线程对多条语句只执行了一部分,还没有执行完, 此时另一个线程参与进来执行,导致共享数据的错误。

 

解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Java 对于多线程的安全提供了专业的解决方式。

线程的同步是保证多线程安全访问竞争资源的一种手段,对于同步,在具体的Java代码中需要完成一下两个操作:

把竞争访问的资源标识为private;
同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。
synchronized(对象){
     代码块
     ...
}

同步的前提:
1、必须要有两个或者两个以上的线程运行;
2、必须是多个线程使用同一个锁;
好处:解决了多线程的安全问题;
弊端:多个线程需要判断锁,较为消耗资源;
注意: 非静态同步函数的对象锁为this,静态同步函数所使用的锁是该方法所在类的字节码文件对象,即类名.class,静态方法里的同步锁都是使用的是类的字节码对象。

 

//静态同步函数锁
public static synchronized void show(){
    ticket++;
    System.out.println(Thread.currentThread().getName()+"runtime..."+ticket--);
}

下面来例举一个线程同步的例子:(同步方法)

package thread;

public class SynchronizedThread { 
    public static void main(String[] args) { 
        User u = new User("王某", 100); 
        MyThread2 t1 = new MyThread2("线程A", u, 10); 
        MyThread2 t2 = new MyThread2("线程B", u, -50); 
        MyThread2 t3 = new MyThread2("线程C", u, -60); 
        MyThread2 t4 = new MyThread2("线程D", u, -40); 
        MyThread2 t5 = new MyThread2("线程E", u, 20); 
        MyThread2 t6 = new MyThread2("线程F", u, 28); 

        t1.start(); 
        t2.start(); 
        t3.start(); 
        t4.start(); 
        t5.start(); 
        t6.start(); 
    } 
} 

class MyThread2 extends Thread { 
    private User u; 
    private int y = 0; 

    MyThread2(String name, User u, int y) { 
        super(name); 
        this.u = u; 
        this.y = y; 
    } 

    public void run() { 
        u.oper(y); 
    } 
} 

class User { 
    private String code; 
    private int cash; 

    User(String code, int cash) { 
        this.code = code; 
        this.cash = cash; 
    } 

    public String getCode() { 
        return code; 
    } 

    public void setCode(String code) { 
        this.code = code; 
    } 

    /** 
     * 业务方法 
     * @param x 添加x万元 
     */ 
    public synchronized void oper(int x) { 
        try { 
            Thread.sleep(10L); 
            this.cash += x; 
            System.out.println(Thread.currentThread().getName() + "运行结束,增加“" + x + "”,当前用户账户余额为:" + cash); 
            Thread.sleep(10L); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    } 

    @Override 
    public String toString() { 
        return "User{" + 
                        "code='" + code + '\'' + 
                        ", cash=" + cash + 
                        '}'; 
    } 
}

【运行结果】如下:

线程A运行结束,增加“10”,当前用户账户余额为:110
线程F运行结束,增加“28”,当前用户账户余额为:138
线程E运行结束,增加“20”,当前用户账户余额为:158
线程D运行结束,增加“-40”,当前用户账户余额为:118
线程C运行结束,增加“-60”,当前用户账户余额为:58
线程B运行结束,增加“-50”,当前用户账户余额为:8

下面是线程不同步的情况,也就是去掉oper(int x)方法的synchronized修饰符,然后再运行程序

【运行结果】如下:

线程F运行结束,增加“28”,当前用户账户余额为:128
线程D运行结束,增加“-40”,当前用户账户余额为:88
线程B运行结束,增加“-50”,当前用户账户余额为:38
线程E运行结束,增加“20”,当前用户账户余额为:58
线程C运行结束,增加“-60”,当前用户账户余额为:-2
线程A运行结束,增加“10”,当前用户账户余额为:8

很显然,上面的结果是错误的,导致错误的原因是多个线程并发访问了竞争资源u,并对u的属性做了改动。

注意:当去掉synchronized修饰符后,线程不在同步,每次运行的结果将都不一样,可见同步的重要性。

 

再把以上实例改为同步代码块方式

 

对于同步,除了同步方法外,还可以使用同步代码块,有时候同步代码块会带来比同步方法更好的效果。

追其同步的根本的目的,是控制竞争资源的正确的访问,因此只要在访问竞争资源的时候保证同一时刻只能一个线程访问即可,因此Java引入了同步代码快的策略,以提高性能。

 

在上个例子的基础上,对oper方法做了改动,由同步方法改为同步代码块模式。代码如下:

package thread;
/**
 * 同步代码块
 * @author Chu
 *
 */
public class SynchronizedThread2 { 
    public static void main(String[] args) { 
            User u = new User("张三", 100); 
            MyThread3 t1 = new MyThread3("线程A", u, 10); 
            MyThread3 t2 = new MyThread3("线程B", u, -50); 
            MyThread3 t3 = new MyThread3("线程C", u, -60); 
            MyThread3 t4 = new MyThread3("线程D", u, -40); 
            MyThread3 t5 = new MyThread3("线程E", u, 20); 
            MyThread3 t6 = new MyThread3("线程F", u, 28);

            t1.start(); 
            t2.start(); 
            t3.start(); 
            t4.start(); 
            t5.start(); 
            t6.start(); 
    } 
} 

class MyThread3 extends Thread { 
    private User u; 
    private int y = 0; 

    MyThread3(String name, User u, int y) { 
        super(name); 
        this.u = u; 
        this.y = y; 
    } 

    public void run() { 
        u.oper(y); 
    } 
} 

class User2 { 
    private String code; 
    private int cash; 

    User2(String code, int cash) { 
        this.code = code; 
        this.cash = cash; 
    } 

    public String getCode() { 
        return code; 
    } 

    public void setCode(String code) { 
        this.code = code; 
    } 

    /** 
     * 业务方法 
     * @param x 添加x万元 
     */ 
    public void oper(int x) { 
        try { 
            Thread.sleep(10L); 
            synchronized (this) { 
                    this.cash += x; 
                    System.out.println(Thread.currentThread().getName() + "运行结束,增加“" + x + "”,当前用户账户余额为:" + cash); 
            } 
            Thread.sleep(10L); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    } 

    @Override 
    public String toString() { 
        return "User{" + 
                        "code='" + code + '\'' + 
                        ", cash=" + cash + 
                        '}'; 
    } 
}

【运行结果】如下:

线程A运行结束,增加“10”,当前用户账户余额为:110
线程F运行结束,增加“28”,当前用户账户余额为:138
线程D运行结束,增加“-40”,当前用户账户余额为:98
线程E运行结束,增加“20”,当前用户账户余额为:118
线程C运行结束,增加“-60”,当前用户账户余额为:58
线程B运行结束,增加“-50”,当前用户账户余额为:8

用到线程的同步,随之可能会带来死锁问题。

导致死锁的原因:两个线程互相等待竞争资源,导致两边都无法得到资源,而使自己无法运行。

下面例举一个导致死锁的一个实例,代码如下:

package thread;

class Demo1{
    static Object obj1=new Object();
    static Object obj2=new Object();
}

class Demo2 implements Runnable{
    boolean flag;
    Demo2(boolean flag){
    this.flag=flag;
}
    @Override
    public void run(){
        if(flag){
            while(true){
                synchronized(Demo1.obj1){
                    System.out.println("1");
                    synchronized(Demo1.obj2){
                        System.out.println("2");
                    }
                }
            }
        }
        else{
            while(true){
                synchronized(Demo1.obj2){
                    System.out.println("2");
                    synchronized(Demo1.obj1){
                        System.out.println("1");
                    }
                }
            }
        }    
    }    
}

最后我再说说:生产者消费者的问题

对于多线程程序来说,不管任何编程语言,生产者和消费者模型都是最经典的。

实际上,准确说应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力了。
对于此模型,应该明确一下几点:
1、生产者仅仅在仓储未满时候生产,仓满则停止生产;
2、消费者仅仅在仓储有产品时候才能消费,仓空则等待;
3、当消费者发现仓储没产品可消费时候会通知生产者生产;
4、生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

此模型将要结合java.lang.Object的wait与notify、notifyAll方法来实现以上的需求。这是非常重要的。

 

具体实现代码如下:

package thread;
/** 
* Java线程:生产者消费者模型 
* @author Chu 2013-06-15 05:32:29 
*/ 
public class ProductTest { 
    public static void main(String[] args) { 
        Godown godown = new Godown(20); 
        Consumer c1 = new Consumer(80, godown); 
        Consumer c2 = new Consumer(30, godown); 
        Consumer c3 = new Consumer(20, godown); 
        Producer p1 = new Producer(5, godown); 
        Producer p2 = new Producer(5, godown); 
        Producer p3 = new Producer(5, godown); 
        Producer p4 = new Producer(10, godown); 
        Producer p5 = new Producer(20, godown); 
        Producer p6 = new Producer(35, godown); 
        Producer p7 = new Producer(50, godown); 

        c1.start(); 
        c2.start(); 
        c3.start(); 
        p1.start(); 
        p2.start(); 
        p3.start(); 
        p4.start(); 
        p5.start(); 
        p6.start(); 
        p7.start(); 
    } 
} 

/** 仓库   */ 
class Godown { 
    public static final int max_size = 100; //最大库存量 
    public int curnum;     //当前库存量 

    Godown() { 
    } 

    Godown(int curnum) { 
        this.curnum = curnum; 
    } 
    /** 
     * 生产指定数量的产品 
     * @param neednum 
     */ 
    public synchronized void produce(int neednum) { 
        //测试是否需要生产 
        while (neednum + curnum > max_size) { 
            System.out.println("要生产的产品数量" + neednum + "超过剩余库存量" + (max_size - curnum) + ",暂时不能执行生产任务!"); 
            try { 
                //当前的生产线程等待 
                wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
        } 
        //满足生产条件,则进行生产,这里简单的更改当前库存量 
        curnum += neednum; 
        System.out.println("已经生产了" + neednum + "个产品,现仓储量为" + curnum); 
        //唤醒在此对象监视器上等待的所有线程 
        notifyAll(); 
    } 

    /** 
     * 消费指定数量的产品 
     * @param neednum 
     */ 
    public synchronized void consume(int neednum) { 
        //测试是否可消费 
        while (curnum < neednum) { 
            try { 
                //当前的生产线程等待 
                wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
        } 
        //满足消费条件,则进行消费,这里简单的更改当前库存量 
        curnum -= neednum; 
        System.out.println("已经消费了" + neednum + "个产品,现仓储量为" + curnum); 
        //唤醒在此对象监视器上等待的所有线程 
        notifyAll(); 
    } 
} 

/** 生产者   */ 
class Producer extends Thread { 
    //生产产品的数量 
    private int neednum; 
    //仓库 
    private Godown godown;

    Producer(int neednum, Godown godown) { 
        this.neednum = neednum; 
        this.godown = godown; 
    } 

    public void run() { 
        //生产指定数量的产品 
        godown.produce(neednum); 
    } 
} 

/** 消费者    */ 
class Consumer extends Thread {
    //生产产品的数量
    private int neednum;
    //仓库 
    private Godown godown; 

    Consumer(int neednum, Godown godown) { 
        this.neednum = neednum; 
        this.godown = godown; 
    } 

    public void run() { 
        //消费指定数量的产品 
        godown.consume(neednum); 
    } 
}

【运行结果】如下:

已经消费了20个产品,现仓储量为0
已经生产了5个产品,现仓储量为5
已经生产了5个产品,现仓储量为10
已经生产了5个产品,现仓储量为15
已经生产了20个产品,现仓储量为35
已经生产了50个产品,现仓储量为85
已经消费了80个产品,现仓储量为5
已经生产了10个产品,现仓储量为15
已经生产了35个产品,现仓储量为50
已经消费了30个产品,现仓储量为20
说明:
对于本例,要说明的是当发现不能满足生产或者消费条件的时候,调用对象的wait方法,wait方法的作用是释放当前线程的所获得的锁,并调用对象的notifyAll() 方法,通知(唤醒)该对象上其他等待线程,使得其继续执行。这样,整个生产者、消费者线程得以正确的协作执行。
notifyAll() 方法,起到的是一个通知作用,不释放锁,也不获取锁。只是告诉该对象上等待的线程可以竞争执行了。

以上这个例子仅仅是生产者消费者模型中最简单的一种表示,在这个例子中,如果消费者消费的仓储量达不到满足,而又没有生产者,则程序会一直处于等待状态,这当然是不对的。实际上可以将此例进行修改,修改为,根据消费驱动生产,同时生产兼顾仓库,如果仓不满就生产,并对每次最大消费量做个限制,这样就不存在此问题了,当然这样的例子更复杂,更难以说明这样一个简单模型。

 

转自:http://wangqiang6028.iteye.com/blog/1887342

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!