深刻浅出Java多线程

初遇

Java给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称做一个线程,而且每一个线程定义了一个独立的执行路径。html

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。前端

这里定义和线程相关的另外一个术语 - 进程:一个进程包括由操做系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到全部的非守候线程都结束运行后才能结束。java

多线程能知足程序员编写高效率的程序来达到充分利用CPU的目的。程序员

1. 多线程基础概念介绍

进程是程序(任务)的执行过程,它持有资源(共享内存,共享文件)和线程算法

分析:编程

执行过程 是动态性的,你放在电脑磁盘上的某个eclipse或者QQ文件并非咱们的进程,只有当你双击运行可执行文件,使eclipse或者QQ运行以后,这才称为进程。它是一个执行过程,是一个动态的概念。安全

它持有资源(共享内存,共享文件)和线程:咱们说进程是资源的载体,也是线程的载体。这里的资源能够理解为内存。咱们知道程序是要从内存中读取数据进行运行的,因此每一个进程得到执行的时候会被分配一个内存。微信

③ 线程是什么?
这里写图片描述多线程

若是咱们把进程比做一个班级,那么班级中的每一个学生能够将它视做一个线程。学生是班级中的最小单元,构成了班级中的最小单位。一个班级有能够多个学生,这些学生都使用共同的桌椅、书籍以及黑板等等进行学习和生活。并发

在这个意义上咱们说:

线程是系统中最小的执行单元;同一进程中能够有多个线程;线程共享进程的资源。

④ 线程是如何交互?

就如同一个班级中的多个学生同样,咱们说多个线程须要通讯才能正确的工做,这种通讯,咱们称做线程的交互

交互的方式:互斥、同步

类比班级,就是在同一班级以内,同窗之间经过相互的协做才能完成某些任务,有时这种协做是须要竞争的,好比学习,班级以内公共的学习资料是有限的,爱学习的同窗须要抢占它,须要竞争,当一个同窗使用完了以后另外一个同窗才可使用;若是一个同窗正在使用,那么其余新来的同窗只能等待;另外一方面须要同步协做,就比如班级六一须要排演节目,同窗须要齐心合力相互配合才能将节目演好,这就是进程交互。

一个线程的生命周期

线程通过其生命周期的各个阶段。下图显示了一个线程完整的生命周期。
这里写图片描述

  • 新建状态:

使用 new 关键字和 Thread 类或其子类创建一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

当线程对象调用了start()方法以后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    若是就绪状态的线程获取 CPU 资源,就能够执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它能够变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

若是一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源以后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或得到设备资源后能够从新进入就绪状态。

  • 死亡状态:

一个运行状态的线程完成任务或者其余终止条件发生时,该线程就切换到终止状态。

线程的状态转换图

这里写图片描述

一、新建状态(New):新建立了一个线程对象。

二、就绪状态(Runnable):线程对象建立后,其余线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

三、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

四、阻塞状态(Blocked):阻塞状态是线程由于某种缘由放弃CPU使用权,暂时中止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的状况分三种:

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

(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

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

五、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程的调度

一、调整线程优先级:

每个Java线程都有一个优先级,这样有助于操做系统肯定线程的调度顺序。

Java线程的优先级用整数表示,取值范围是1~10,Thread类有如下三个静态常量:
static int MAX_PRIORITY

线程能够具备的最高优先级,取值为10。

static int MIN_PRIORITY

线程能够具备的最低优先级,取值为1。

static int NORM_PRIORITY

分配给线程的默认优先级,取值为5。

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每一个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,好比A线程中建立了B线程,那么B将和A具备相同的优先级。
JVM提供了10个线程优先级,但与常见的操做系统都不能很好的映射。若是但愿程序能移植到各个操做系统中,应该仅仅使用Thread类有如下三个静态常量做为优先级,这样能保证一样的优先级采用了一样的调度方式。

具备较高优先级的线程对程序更重要,而且应该在低优先级的线程以前分配处理器资源。可是,线程优先级不能保证线程执行的顺序,并且很是依赖于平台。

二、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

三、线程等待:Object类中的wait()方法,致使当前的线程等待,直到其余线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 同样。

四、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

五、线程加入:join()方法,等待其余线程终止。在当前线程中调用另外一个线程的join()方法,则当前线程转入阻塞状态,直到另外一个进程运行结束,当前线程再由阻塞转为就绪状态。

六、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。若是全部线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现作出决定时发生。线程经过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其余全部线程进行竞争;例如,唤醒的线程在做为锁定此对象的下一个线程方面没有可靠的特权或劣势。相似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的全部线程。

注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,再也不介绍。由于有死锁倾向。

七、常见线程名词解释
主线程:JVM调用程序main()所产生的线程。
当前线程:这个是容易混淆的概念。通常指经过Thread.currentThread()来获取的进程。
后台线程:指为其余线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。
前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一块儿,就像傀儡和幕后操纵者同样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程建立的线程默认也是前台线程。能够经过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。

一些常见问题

一、线程的名字,一个运行中的线程老是有名字的,名字有两个来源,一个是虚拟机本身给的名字,一个是你本身的定的名字。在没有指定线程名字的状况下,虚拟机总会为线程指定名字,而且主线程的名字老是main,非主线程的名字不肯定。

二、线程均可以设置名字,也能够获取线程的名字,连主线程也不例外。

三、获取当前线程的对象的方法是:Thread.currentThread();

四、每一个线程都将启动,每一个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来讲,调度程序不能保证其执行次序,持续时间也没法保证。

五、当线程目标run()方法结束时该线程完成。

六、一旦线程启动,它就永远不能再从新启动。只有一个新的线程能够被启动,而且只能一次。一个可运行的线程或死线程能够被从新启动。

七、线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪一个处于可运行状态的线程。
众多可运行线程中的某一个会被选中作为当前线程。可运行线程被选择运行的顺序是没有保障的。

八、尽管一般采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,咱们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列唱呢个一个队列的事实。

九、尽管咱们没有没法控制线程调度程序,但能够经过别的方式来影响线程调度的方式。

2. Java 中线程的经常使用方法介绍

Java语言对线程的支持

主要体如今Thread类Runnable接口上,都继承于java.lang包。它们都有个共同的方法:public void run()

  run方法为咱们提供了线程实际工做执行的代码。

下表列出了Thread类的一些重要方法:

序号 方法描述
1 public void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2 public void run()若是该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;不然,该方法不执行任何操做并返回。
3 public final void setName(String name)改变线程名称,使之与参数 name 相同。
4 public final void setPriority(int priority)更改线程的优先级。
5 public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。
6 public final void join(long millisec)等待该线程终止的时间最长为 millis 毫秒。
7 public void interrupt()中断线程。
8 public final boolean isAlive()测试线程是否处于活动状态。

测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

序号 方法描述
1 public static void yield()暂停当前正在执行的线程对象,并执行其余线程。
2 public static void sleep(long millisec)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操做受到系统计时器和调度程序精度和准确性的影响。
3 public static boolean holdsLock(Object x)当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4 public static Thread currentThread()返回对当前正在执行的线程对象的引用。
5 public static void dumpStack()将当前线程的堆栈跟踪打印至标准错误流。

Thread经常使用的方法

这里写图片描述

3. 线程初体验(编码示例)

建立线程的方法有两种:

1.继承Thread类自己

2.实现Runnable接口

线程中的方法比较有特色,好比:启动(start),休眠(sleep),中止等,多个线程是交互执行的(cpu在某个时刻。只能执行一个线程,当一个线程休眠了或者执行完毕了,另外一个线程才能占用cpu来执行)由于这是cpu的结构来决定的,在某个时刻cpu只能执行一个线程,不过速度至关快,对于人来将能够认为是并行执行的。

在一个java文件中,能够有多个类(此处说的是外部类),但只能有一个public类。

这两种建立线程的方法本质没有任何的不一样,一个是实现Runnable接口,一个是继承Thread类。

使用实现Runnable接口这种方法:

  1.能够避免java的单继承的特性带来的局限性;

  2.适合多个相同程序的代码去处理同一个资源状况,把线程同程序的代码及数据有效的分离,较好的体现了面向对象的设计思想。开发中大多数状况下都使用实现Runnable接口这种方法建立线程。

实现Runnable接口建立的线程最终仍是要经过将自身实例做为参数传递给Thread而后执行

语法: Thread actress=new Thread(Runnable target ,String name);

例如:

Thread actressThread=new Thread(new Actress(),"Ms.runnable");
actressThread.start();

代码示例:

package com.study.thread;

public class Actor extends Thread{
    public void run() {
        System.out.println(getName() + "是一个演员!");
        int count = 0;
        boolean keepRunning = true;

        while(keepRunning){
            System.out.println(getName()+"登台演出:"+ (++count));
            if(count == 100){
                keepRunning = false;
            }
            if(count%10== 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(getName() + "的演出结束了!");
    }

    public static void main(String[] args) {
       Thread actor = new Actor();//向上转型:子类转型为父类,子类对象就会遗失和父类不一样的方法。向上转型符合Java提倡的面向抽象编程思想,还能够减轻编程工做量
       actor.setName("Mr. Thread");
       actor.start();
       
       //调用Thread的构造函数Thread(Runnable target, String name)
       Thread actressThread = new Thread(new Actress(), "Ms. Runnable");
       actressThread.start();
    }

}
//注意:在“xx.java”文件中能够有多个类,可是只能有一个Public类。这里所说的不是内部类,都是一个个独立的外部类
class Actress implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "是一个演员!");//Runnable没有getName()方法,须要经过线程的currentThread()方法得到线程名称
        int count = 0;
        boolean keepRunning = true;

        while(keepRunning){
            System.out.println(Thread.currentThread().getName()+"登台演出:"+ (++count));
            if(count == 100){
                keepRunning = false;
            }
            if(count%10== 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "的演出结束了!");
    }
    
}

/**
 *运行结果Mr. Thread线程和Ms. Runnable线程是交替执行的状况
 *分析:计算机CPU处理器在同一时间同一个处理器同一个核只能运行一条线程,
 *当一条线程休眠以后,另一个线程才得到处理器时间
 */

运行结果:
这里写图片描述
示例2:

ArmyRunnable 类:

package com.study.threadTest1;

/**
 * 军队线程
 * 模拟做战双方的行为
 */
public class ArmyRunnable implements Runnable {

    /* volatile关键字
     * volatile保证了线程能够正确的读取其余线程写入的值
     * 若是不写成volatile,因为可见性的问题,当前线程有可能不能读到这个值
     * 关于可见性的问题能够参考JMM(Java内存模型),里面讲述了:happens-before原则、可见性
     * 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的值
     */
    volatile boolean keepRunning = true;

    @Override
    public void run() {
        while (keepRunning) {
            //发动5连击
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"进攻对方["+i+"]");
                //让出了处理器时间,下次该谁进攻还不必定呢!
                Thread.yield();//yield()当前运行线程释放处理器资源
            } 
        }
        System.out.println(Thread.currentThread().getName()+"结束了战斗!");
    }

}

KeyPersonThread 类:

package com.study.threadTest1;


public class KeyPersonThread extends Thread {
    public void run(){
        System.out.println(Thread.currentThread().getName()+"开始了战斗!");
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"左突右杀,攻击隋军...");
        }
        System.out.println(Thread.currentThread().getName()+"结束了战斗!");
    }

}

Stage 类:

package com.study.threadTest1;

/**
 * 隋唐演义大戏舞台 6  */
public class Stage extends Thread {
    public void run(){
        System.out.println("欢迎观看隋唐演义");
        //让观众们安静片刻,等待大戏上演
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.out.println("大幕徐徐拉开");

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("话说隋朝末年,隋军与农民起义军杀得昏天黑地...");
        ArmyRunnable armyTaskOfSuiDynasty = new ArmyRunnable();
        ArmyRunnable armyTaskOfRevolt = new ArmyRunnable();

        //使用Runnable接口建立线程
        Thread  armyOfSuiDynasty = new Thread(armyTaskOfSuiDynasty,"隋军");
        Thread  armyOfRevolt = new Thread(armyTaskOfRevolt,"农民起义军");

        //启动线程,让军队开始做战
        armyOfSuiDynasty.start();
        armyOfRevolt.start();

        //舞台线程休眠,你们专心观看军队厮杀
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("正当双方激战正酣,半路杀出了个程咬金");

        Thread  mrCheng = new KeyPersonThread();
        mrCheng.setName("程咬金");
        System.out.println("程咬金的理想就是结束战争,使百姓安居乐业!");

        //中止军队做战
        //中止线程的方法
        armyTaskOfSuiDynasty.keepRunning = false;
        armyTaskOfRevolt.keepRunning = false;

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        /*
         * 历史大戏留给关键人物
         */
        mrCheng.start();

        //万众瞩目,全部线程等待程先生完成历史使命
        try {
            mrCheng.join();//join()使其余线程等待当前线程终止
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("战争结束,人民安居乐业,程先生实现了积极的人生梦想,为人民做出了贡献!");
        System.out.println("谢谢观看隋唐演义,再见!");
    }

    public static void main(String[] args) {
        new Stage().start();
    }

}

运行结果:
这里写图片描述

4. Java 线程的正确中止

如何正确的中止Java中的线程?

stop方法:该方法使线程戛然而止(忽然中止),完成了哪些工做,哪些工做尚未作都不清楚,且清理工做也没有作。

stop方法不是正确的中止线程方法。线程中止不推荐使用stop方法。

正确的方法---设置退出标志

使用volatile 定义boolean running=true,经过设置标志变量running,来结束线程。

如本文:volatile boolean keepRunning=true;

这样作的好处是:使得线程有机会使得一个完整的业务步骤被完整地执行,在执行完业务步骤后有充分的时间去作代码的清理工做,使得线程代码在实际中更安全。
这里写图片描述

广为流传的错误方法---interrupt方法

这里写图片描述
当一个线程运行时,另外一个线程能够调用对应的 Thread 对象的 interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并当即返回。这里须要注意的是,若是只是单纯的调用 interrupt()方法,线程并无实际被中断,会继续往下执行。

代码示例:

package com.study.threadStop;

/**
 * 错误终止进程的方式——interrupt
 */
public class WrongWayStopThread extends Thread {

    public static void main(String[] args) {
        WrongWayStopThread thread = new WrongWayStopThread();
        System.out.println("Start Thread...");
        thread.start();
        
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Interrupting thread...");
        thread.interrupt();
        
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Stopping application...");
    }
    
    public void run() {
        while(true){
            System.out.println("Thread is running...");
            long time = System.currentTimeMillis();
            while ((System.currentTimeMillis()-time) <1000) {//这部分的做用大体至关于Thread.sleep(1000),注意此处为何没有使用休眠的方法
                //减小屏幕输出的空循环(使得每秒钟只输出一行信息)
            }
        }
    }
}

运行结果:
这里写图片描述
由结果看到interrupt()方法并无使线程中断,线程仍是会继续往下执行。

Java API中介绍:
这里写图片描述
可是interrupt()方法可使咱们的中断状态发生改变,能够调用isInterrupted 方法
这里写图片描述
将上处run方法代码改成下面同样,程序就能够正常结束了。

public void run() {
        while(!this.isInterrupted()){//interrupt()可使中断状态放生改变,调用isInterrupted()
            System.out.println("Thread is running...");
            long time = System.currentTimeMillis();
            while ((System.currentTimeMillis()-time) <1000) {//这部分的做用大体至关于Thread.sleep(1000),注意此处为何没有使用休眠的方法
                //减小屏幕输出的空循环(使得每秒钟只输出一行信息)
            }
        }
    }

可是这种所使用的退出方法实质上仍是前面说的使用退出旗标的方法,不过这里所使用的退出旗标是一个特殊的标志“线程是否被中断的状态”。
这里写图片描述
这部分代码至关于线程休眠1秒钟的代码。可是为何没有使用Thread.sleep(1000)。若是采用这种方法就会出现
这里写图片描述
线程没有正常结束,并且还抛出了一个异常,异常抛出位置在调用interrupt方法以后。为何会有这种结果?

在API文档中说过:若是线程因为调用的某些方法(好比sleep,join。。。)而进入一种阻塞状态时,此时若是这个线程再被调用interrupt方法,它会产生两个结果:第一,它的中断状态被清除clear,而不是被设置set。那isInterrupted 就不能返回是否被中断的正确状态,那while函数就不能正确的退出。第二,sleep方法会收到InterruptedException被中断。

interrupt()方法只能设置interrupt标志位(且在线程阻塞状况下,标志位会被清除,更没法设置中断标志位),没法中止线程

5. 线程交互

争用条件:

一、当多个线程同时共享访问同一数据(内存区域)时,每一个线程都尝试操做该数据,从而致使数据被破坏(corrupted),这种现象称为争用条件

二、缘由是,每一个线程在操做数据时,会先将数据初值读【取到本身得到的内存中】,而后在内存中进行运算后,从新赋值到数据。

三、争用条件:线程1在还【未从新将值赋回去时】,线程1阻塞,线程2开始访问该数据,而后进行了修改,以后被阻塞的线程1再得到资源,而将以前计算的值覆盖掉线程2所修改的值,就出现了数据丢失状况。

互斥与同步:守恒的能量

一、线程的特色,共享同一进程的资源,同一时刻只能有一个线程占用CPU

二、因为线程有如上的特色,因此就会存在多个线程争抢资源的现象,就会存在争用条件这种现象

三、为了让线程可以正确的运行,不破坏共享的数据,因此,就产生了同步和互斥的两种线程运行的机制

四、线程的互斥(加锁实现):线程的运行隔离开来,互不影响,使用synchronized关键字实现互斥行为,此关键字便可以出如今方法体之上也能够出如今方法体内,以一种块的形式出现,在此代码块中有线程的等待和唤醒动做,用于支持线程的同步控制

五、线程的同步(线程的等待和唤醒:wait()+notifyAll()):线程的运行有相互的通讯控制,运行完一个再正确的运行另外一个

六、锁的概念:好比private final Object lockObj=new Object();

七、互斥实现方式:synchronized关键字

synchronized(lockObj){---执行代码----}加锁操做

lockObj.wait();线程进入等待状态,以免线程持续申请锁,而不去竞争cpu资源

lockObj.notifyAll();唤醒全部lockObj对象上等待的线程

八、加锁操做会开销系统资源,下降效率

同步问题提出

线程的同步是为了防止多个线程访问一个数据对象时,对数据形成的破坏。
例如:两个线程ThreadA、ThreadB都操做同一个对象Foo对象,并修改Foo对象上的数据。

public class Foo { 
    private int x = 100; 

    public int getX() { 
        return x; 
    } 

    public int fix(int y) { 
        x = x - y; 
        return x; 
    } 
}
public class MyRunnable implements Runnable { 
    private Foo foo = new Foo(); 

    public static void main(String[] args) { 
        MyRunnable r = new MyRunnable(); 
        Thread ta = new Thread(r, "Thread-A"); 
        Thread tb = new Thread(r, "Thread-B"); 
        ta.start(); 
        tb.start(); 
    } 

    public void run() { 
        for (int i = 0; i < 3; i++) { 
            this.fix(30); 
            try { 
                Thread.sleep(1); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println(Thread.currentThread().getName() + " : 当前foo对象的x值= " + foo.getX()); 
        } 
    } 

    public int fix(int y) { 
        return foo.fix(y); 
    } 
}

运行结果:

Thread-A : 当前foo对象的x值= 40 
Thread-B : 当前foo对象的x值= 40 
Thread-B : 当前foo对象的x值= -20 
Thread-A : 当前foo对象的x值= -50 
Thread-A : 当前foo对象的x值= -80 
Thread-B : 当前foo对象的x值= -80 

Process finished with exit code 0

从结果发现,这样的输出值明显是不合理的。缘由是两个线程不加控制的访问Foo对象并修改其数据所致。

若是要保持结果的合理性,只须要达到一个目的,就是将对Foo的访问加以限制,每次只能有一个线程在访问。这样就能保证Foo对象中数据的合理性了。

在具体的Java代码中须要完成一下两个操做:
把竞争访问的资源类Foo变量x标识为private;
同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。

同步和锁定

一、锁的原理

Java中每一个对象都有一个内置锁
当程序运行到非静态的synchronized同步方法上时,自动得到与正在执行代码类的当前实例(this实例)有关的锁。得到一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起做用。
一个对象只有一个锁。因此,若是一个线程得到该锁,就没有其余线程能够得到锁,直到第一个线程释放(或返回)锁。这也意味着任何其余线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。

关于锁和同步,有一下几个要点:

1)、只能同步方法,而不能同步变量和类;

2)、每一个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪一个对象上同步?

3)、没必要同步类中全部的方法,类能够同时拥有同步和非同步方法。

4)、若是两个线程要执行一个类中的synchronized方法,而且两个线程使用相同的实例来调用方法,那么一次只能有一个线程可以执行方法,另外一个须要等待,直到锁被释放。也就是说:若是一个线程在对象上得到一个锁,就没有任何其余线程能够进入(该对象的)类中的任何一个同步方法。

5)、若是线程拥有同步和非同步方法,则非同步方法能够被多个线程自由访问而不受锁的限制。

6)、线程睡眠时,它所持的任何锁都不会释放。

7)、线程能够得到多个锁。好比,在一个对象的同步方法里面调用另一个对象的同步方法,则获取了两个对象的同步锁。

8)、同步损害并发性,应该尽量缩小同步范围。同步不但能够同步整个方法,还能够同步方法中一部分代码块。

9)、在使用同步代码块时候,应该指定在哪一个对象上同步,也就是说要获取哪一个对象的锁。例如:

public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }

固然,同步方法也能够改写为非同步方法,但功能彻底同样的,例如:

public synchronized int getX() {
        return x++;
    }

public int getX() {
        synchronized (this) {
            return x;
        }
    }

效果是彻底同样的。

静态方法同步

要同步静态方法,须要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。
例如:

public static synchronized int setName(String name){
      Xxx.name = name;
}

等价于

public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

线程同步小结

一、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。

二、线程同步方法是经过锁来实现,每一个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其余访问该对象的线程就没法再访问该对象的其余同步方法。

三、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程得到锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

四、对于同步,要时刻清醒在哪一个对象上同步,这是关键。

五、编写线程安全的类,须要时刻注意对多个线程竞争访问资源的逻辑和安全作出正确的判断,对“原子”操做作出分析,并保证原子操做期间别的线程没法访问竞争资源。

六、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

七、死锁是线程间相互等待锁锁形成的,在实际中发生的几率很是的小。真让你写个死锁程序,不必定好使,呵呵。可是,一旦程序发生死锁,程序将死掉。

深刻剖析互斥与同步

互斥的实现(加锁):synchronized(lockObj); 保证的同一时间,只有一个线程得到lockObj.

同步的实现:wait()/notify()/notifyAll()

注意: wait()、notify()、notifyAll()方法均属于Object对象,而不是Thread对象。

  • void notify()

唤醒在此对象监视器上等待的单个线程。

  • void notifyAll()

唤醒在此对象监视器上等待的全部线程。

  • void wait()

致使当前的线程等待,直到其余线程调用此对象的 notify() 方法或 notifyAll() 方法。

固然,wait()还有另外两个重载方法:

  • void wait(long timeout)

致使当前的线程等待,直到其余线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。

  • void wait(long timeout, int nanos)
    致使当前的线程等待,直到其余线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其余某个线程中断当前线程,或者已超过某个实际时间量。

notify()唤醒wait set中的一条线程,而notifyall()唤醒全部线程。

同步是两个线程之间的一种交互的操做(一个线程发出消息另一个线程响应)
关于等待/通知,要记住的关键点是:
必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
wait()、notify()、notifyAll()都是Object的实例方法。与每一个对象具备锁同样,每一个对象能够有一个线程列表,他们等待来自该信号(通知)。线程经过执行对象上的wait()方法得到这个等待列表。从那时候起,它再也不执行任何其余指令,直到调用对象的notify()方法为止。若是多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。若是没有线程等待,则不采起任何特殊操做。
下面看个例子就明白了:

/** 
* 计算输出其余线程锁计算的数据 
*/ 
public class ThreadA { 
    public static void main(String[] args) { 
        ThreadB b = new ThreadB(); 
        //启动计算线程 
        b.start(); 
        //线程A拥有b对象上的锁。线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者 
        synchronized (b) { 
            try { 
                System.out.println("等待对象b完成计算。。。"); 
                //当前线程A等待 
                b.wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println("b对象计算的总和是:" + b.total); 
        } 
    } 
}
/** 
* 计算1+2+3 ... +100的和 
*/ 
public class ThreadB extends Thread { 
    int total; 

    public void run() { 
        synchronized (this) { 
            for (int i = 0; i < 101; i++) { 
                total += i; 
            } 
            //(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程A被唤醒 
            notify(); 
        } 
    } 
}
结果:
等待对象b完成计算。。。
b对象计算的总和是:5050
Process finished with exit code 0

千万注意:
当在对象上调用wait()方法时,执行该代码的线程当即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。若是线程荣然在完成同步代码,则线程在移出以前不会放弃锁。所以,只要调用notify()并不意味着这时该锁变得可用。

多个线程在等待一个对象锁时候使用notifyAll():
在多数状况下,最好通知等待某个对象的全部线程。若是这样作,能够在对象上使用notifyAll()让全部在此对象上等待的线程冲出等待区,返回到可运行状态。

如何理解同步:Wait Set

Critical Section(临界资源)Wait Set(等待区域)

wait set 相似于线程的休息室,访问共享数据的代码称为critical section。一个线程获取锁,而后进入临界区,发现某些条件不知足,而后调用锁对象上的wait方法,而后线程释放掉锁资源,进入锁对象上的wait set。因为线程释放释放了理解资源,其余线程能够获取所资源,而后执行,完了之后调用notify,通知锁对象上的等待线程。

Ps:若调用notify();则随机拿出(这随机拿出是内部的算法,无需了解)一条在等待的资源进行准备进入Critical Section;若调用notifyAll();则所有取出进行准备进入Critical Section。

6. 总结与展望

这里写图片描述
这里写图片描述
扩展建议:如何扩展Java并发知识

一、Java Memory Mode : JMM描述了java线程如何经过内存进行交互,了解happens-before , synchronized,voliatile & final

二、Locks % Condition:Java锁机制和等待条件的高层实现 java.util,concurrent.locks

三、线程安全性:原子性与可见性, java.util.concurrent.atomic synchronized(锁的方法块)&volatile(定义公共资源) DeadLocks(死锁)--了解什么是死锁,死锁产生的条件

四、多线程编程经常使用的交互模型

· Producer-Consumer模型(生产者-消费者模型)

· Read-Write Lock模型(读写锁模型)

· Future模型

· Worker Thread模型

考虑在Java并发实现当中,有哪些类实现了这些模型,供咱们直接调用

五、Java5中并发编程工具:java.util.concurrent 包下的

例如:线程池ExcutorService 、Callable&Future 、BlockingQueue

六、推荐书本:CoreJava 、JavaConcurrency In Practice

文章有不当之处,欢迎指正,你也能够关注个人微信公众号: 好好学java,获取优质学习资源。
相关文章
相关标签/搜索