<安彦>深入浅出多线程技术

一.概念引入

1.进程与线程区别

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

进程是受操作系统管理的基本运行单元
线程:进程中的一个执行流程,一个进程中可以运行多个线程。线程总是属于某个线程,进程中的多个线程共享进程的内存。
注;“同时”执行是人的感觉,在线程之间实际上轮换执行。

2.并行和并发区别
并行:就是两个任务同时运行,就是甲任务进行的同时,乙任务也在运行(需要多核CPU)。
并发:是指两个任务都请求运行,而处理器只能接受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。

3.Java程序运行原理
Java命令会启动Java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法。

4.JVM的启动与多线程
JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

5.线程的五个阶段
新建、就绪、运行、阻塞、死亡。

6.主线程
Myeclipse运行Java Application(main方法),启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行的程序代码,一直把main方法的代码执行结束。如果在执行过程遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被马上执行的。

package com.yw.web.demo2;

class Demo{
    String name;
    Demo(String name){
        this.name = name;
    }
    void show() {
        for (int i=1;i<=10000 ;i++ )        {
            System.out.println("name="+name+",i="+i);
        }
    }
}
Public class ThreadDemo {
    public static void main(String[] args)  {
        Demo d = new Demo("小强");
         Demo d2 = new Demo("旺财");
        d.show();       
        d2.show();
        System.out.println("Hello World!");
    }
}

原因是:jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。
那么,能否实现一个主线程负责执行其中一个循环,再由另一个线程负责其他代码的执行,最终实现多部分代码同时执行的效果?

能够实现同时执行,通过Java中的多线程技术来解决该问题。

二.多线程技术

1.初始API


Thread类

通过API中搜索,查到Thread类。通过阅读Thread类中的描述。Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
这里写图片描述

构造方法
这里写图片描述
常用方法
这里写图片描述
这里写图片描述
继续阅读,发现创建新执行线程有两种方法。
一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。
另一种方法是声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

创建线程方式:继承Thread类

创建线程的步骤
1.定义一个类继承Thread。
2.重写run方法。
3.创建子类对象,就是创建线程对象。
4.调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
测试类:

package com.yw.web.demo2;
public class Demo01 {
    public static void main(String[] args) {
        //创建自定义线程对象
        MyThread mt = new MyThread("新的线程!");
        //开启新线程
        mt.start();
        //在主方法中执行for循环
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程!"+i);
        }
    }
}

class MyThread extends Thread {
    //定义指定线程名称的构造方法
    public MyThread(String name) {
        //调用父类的String参数的构造方法,指定线程的名称
        super(name);
    }
    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":正在执行!"+i);
        }
    }
}

线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

多线程的内存图解
以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
这里写图片描述
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

获取线程名称
开启的线程都会有自己的独立运行栈内存,查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。
这里写图片描述
Thread.currentThread()获取当前线程对象
Thread.currentThread().getName();获取当前线程对象的名称

package com.yw.web.demo2;
public class ThreadDemo {
    public static void main(String[] args)  {
        //创建两个线程任务
        MyThread d = new MyThread("1");
        MyThread d2 = new MyThread("2");
        d.run();//没有开启新线程, 在主线程调用run方法
        d2.start();//开启一个新线程,新线程调用run方法
    }
}
class MyThread extends Thread {  //继承Thread
    MyThread(String name){
        super(name);
    }   
    public MyThread() {
        super();        
    }
    //复写其中的run方法
    public void run(){
        for (int i=1;i<=20 ;i++ ){
            System.out.println(Thread.currentThread().getName()+",i="+i);
        }
    }
}

通过结果观察,原来主线程的名称:main;自定义的线程:Thread-0,线程多个时,数字顺延。如Thread-1……
进行多线程编程时,不要忘记了Java程序运行是从主线程开始,main方法就是主线程的线程执行内容。

Runable接口
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

查看Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。
这里写图片描述

接口中的方法
这里写图片描述

Thread类构造方法
这里写图片描述

创建线程方式:实现Runnable接口

创建线程的步骤。
1、定义类实现Runnable接口。
2、覆盖接口中的run方法。。
3、创建Thread类的对象
4、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
5、调用Thread类的start方法开启线程。
测试类:

package com.yw.web.demo2;
public class Demo02 {
    public static void main(String[] args) {
        // 创建线程执行目标类对象
        Runnable runn = new MyRunnable();
        // 将Runnable接口的子类对象作为参数传递给Thread类的构造函数
        Thread thread = new Thread(runn);
        Thread thread2 = new Thread(runn);
        // 开启线程
        thread.start();
        thread2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:正在执行!" + i);
        }
    }
}
class MyRunnable implements Runnable {
    // 定义线程要执行的run方法逻辑
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我的线程:正在执行!" + i);
        }
    }
}

实现Runnable的原理
实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。

创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

总结
实现Runnable接口比继承Thread类所具有的优势:
1适合多个相同的程序代码的线程去处理同一个资源。
2.可以避免java中的单继承的限制。
3.增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。


2.进阶提高

线程的匿名内部类使用

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
方式1:创建线程对象时,直接重写Thread类中的run方法

new Thread() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...X...." + x);
                }
            }
        }.start();

方式2:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法

Runnable r = new Runnable() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...Y...." + x);
                }
            }
        };
        new Thread(r).start();

线程状态转换
这里写图片描述

1.新建状态(New):新创建了一个线程对象。
2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4.阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程调度
线程的调度
**1、调整线程优先级:**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类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
**2、线程睡眠:**Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
**3、线程等待:**Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
**4、线程让步:**Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
**5、线程加入:**join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
**6、线程唤醒:**Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,我这里就不做分享了。

线程常用方法使用

1.sleep(long millis)
sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

2.join()
2.join():指等待t线程终止。
使用方式:
join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

Thread t = new AThread(); 
t.start();
t.join();

为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

不加join的情况:

package com.yw.web.demo2;
/**
 *@functon 多线程学习,join
 *@author 安彦
 *@time 2017.9.20
 */
class Thread1 extends Thread{
    private String name;
    public Thread1(String name) {
        super(name);
       this.name=name;
    }
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+name + "运行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }
}
public class Demo02 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
    }
}

运行结果:

这里写图片描述
发现主线程比子线程早结束

加join

package com.yw.web.demo2;
public class Demo02 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "主线程运行开始!");
        Thread1 mTh1 = new Thread1("A");
        Thread1 mTh2 = new Thread1("B");
        mTh1.start();
        mTh2.start();
        try {
            mTh1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            mTh2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "主线程运行结束!");
    }
}
class Thread1 extends Thread {
    private String name;
    public Thread1(String name) {
        super(name);
        this.name = name;
    }
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程" + name + "运行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }
}

运行结果:
这里写图片描述
主线程一定会等子线程都结束了才结束

3.yield()

yield():暂停当前正在执行的线程对象,并执行其他线程。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

package com.yw.web.demo2;
/**
 * @functon 多线程学习 yield
 * @author 安彦
 * @time 2017.9.20
 */
class ThreadYield extends Thread {
    public ThreadYield(String name) {
        super(name);
    }
    @SuppressWarnings("static-access")
    public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println("" + this.getName() + "-----" + i);
            // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
            if (i == 30) {
                this.yield();
            }
        }
    }
}
public class Demo02 {
    public static void main(String[] args) {
        ThreadYield yt1 = new ThreadYield("张三");
        ThreadYield yt2 = new ThreadYield("李四");
        yt1.start();
        yt2.start();
    }
}

运行结果:
第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。
第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。可看上面线程状态转换的图。

sleep()和yield()的区别
时间上:
sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的重点内容线程有可能在进入到可执行状态后马上又被执行。
执行上:
sleep方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为退让,它把运行机会让给了同等优先级的其他线程另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

4.setPriority()重点内容

setPriority(): 更改线程的优先级
  MIN_PRIORITY = 1
   NORM_PRIORITY = 5
MAX_PRIORITY = 10
用法:

Thread1 t1 = new Thread1("t1");
Thread1 t2 = new Thread1("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

5.interrupt()
interrupt():中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭要想结束进程最好的办法就是用sleep()函数的例子程序里那样,在线程类里面用以个boolean型变量来控制run()方法什么时候结束,run()方法一结束,该线程也就结束了。

6. wait()
Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用的例子,
题目要求如下:

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

package com.yw.web.demo2;
/**
 * wait用法
 * @author 安彦
 * @time 2017.9.20 
 */
public class Demo02 implements Runnable {   
    private String name;   
    private Object prev;   
    private Object self;   
    private Demo02(String name, Object prev, Object self) {   
        this.name = name;   
        this.prev = prev;   
        this.self = self;   
    }   
    public void run() {   
        int count = 10;   
        while (count > 0) {   
            synchronized (prev) {   
                synchronized (self) {   
                    System.out.print(name);   
                    count--;  
                    self.notify();   
                }   
                try {   
                    prev.wait();   
                } catch (InterruptedException e) {   
                    e.printStackTrace();   
                }   
            }   
        }   
    }   
    public static void main(String[] args) throws Exception {   
        Object a = new Object();   
        Object b = new Object();   
        Object c = new Object();   
        Demo02 pa = new Demo02("A", c, a);   
        Demo02 pb = new Demo02("B", a, b);   
        Demo02 pc = new Demo02("C", b, c);   
        new Thread(pa).start();
        Thread.sleep(100);  //确保按顺序A、B、C执行
        new Thread(pb).start();
        Thread.sleep(100);  
        new Thread(pc).start();   
        Thread.sleep(100);  
        }   
}  

输出结果:

ABCABCABCABCABCABCABCABCABCABC

先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,
主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。
主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。
程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。

wait和sleep区别
共同点:
1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

不同点:
1. Thread类的方法:sleep(),yield()等
Object的方法:wait()和notify()等
2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
所以sleep()和wait()方法的最大区别是:
    sleep()睡眠时,保持对象锁,仍然占有该锁;
    而wait()睡眠时,释放对象锁。
  但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
sleep()方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
   sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait()方法
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
  wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
  wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

线程常见名词解释
**主线程:**JVM调用程序main()所产生的线程。
当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
线程类的一些常用方法:

  sleep(): 强迫一个线程睡眠N毫秒。
  isAlive(): 判断一个线程是否存活。
  join(): 等待线程终止。
  activeCount(): 程序中活跃的线程数。
  enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
  isDaemon(): 一个线程是否为守护线程。
  setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
  setName(): 为线程设置一个名称。
  wait(): 强迫一个线程等待。
  notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。


3.高级技术

线程池使用

线程池概念
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
这里写图片描述
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

使用线程池方式:Runnable接口
通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。
Executors:线程池创建工厂类
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
ExecutorService:线程池类
Future submit(Runnable task):获取线程池中的某一个线程对象,并执行

Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤
1.创建线程池对象
2.创建Runnable接口子类对象
3.提交Runnable接口子类对象
4.关闭线程池
代码演示:

package com.yw.web.demo2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 
 * @author ANYAN
 *
 */
public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        //创建Runnable实例对象
        MyRunnable r = new MyRunnable();
        //自己创建线程对象的方式
        //Thread t = new Thread(r);
        //t.start(); ---> 调用MyRunnable中的run()
        //从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        //再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中

//关闭线程池
        //service.shutdown();
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个健身教练");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " +Thread.currentThread().getName());
        System.out.println("教我健身,交完后,教练回到了健身房");
    }
}

使用线程池方式:Callable接口
Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。
ExecutorService:线程池类
Future submit(Callable task):获取线程池中的某一个线程对象,并执行线程中的call()方法
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤
1.创建线程池对象
2.创建Callable接口子类对象
3.提交Callable接口子类对象
4.关闭线程池
代码演示:

package com.yw.web.demo2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo2 {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        //创建Callable对象
        MyCallable c = new MyCallable();
        //从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(c);
        //再获取个教练
        service.submit(c);
        service.submit(c);
//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中
//关闭线程池
        //service.shutdown();
    }
}
@SuppressWarnings("rawtypes")
class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("我要一个健身教练:call");
        Thread.sleep(2000);
        System.out.println("教练来了: " +Thread.currentThread().getName());
        System.out.println("教我健身,交完后,教练回到了健身房");
        return null;
    }
}

Synchronized使用

线程安全问题
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
通过一个案例,演示线程的安全问题:
电影院要卖票,模拟电影院的卖票过程。假设要播放的电影是 “战狼II”,本次电影的座位共100个(本场电影只能卖100张票)。
模拟电影院的售票窗口,实现多个窗口同时卖 “战狼II”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
测试类

package com.yw.web.demo3;
public class ThreadDemo {
    public static void main(String[] args) {
        //创建票对象
        Ticket ticket = new Ticket();
        //创建3个窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    @Override
    public void run() {
        //模拟卖票
        while(true){
            if (ticket > 0) {
                //模拟选坐的操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
            }
        }
    }
}

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

运行结果发现:上面程序出现了问题
1.票可能出现了重复的票
2.错误的票 0、-1

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程安全处理Synchronized
java中提供了线程同步机制,它能够解决上述的线程安全问题。
线程同步的方式有两种:
方式1:同步代码块
方式2:同步方法
同步代码块
同步代码块: 在代码块声明上 加上synchronized

synchronized (锁对象) {
    //可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对电影卖票案例中Ticket类进行如下代码修改:

package com.yw.web.demo3;
public class ThreadDemo {
    public static void main(String[] args) {
        //创建票对象
        Ticket ticket = new Ticket();
        //创建3个窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket implements Runnable {
    //共100票
        int ticket = 100;
        //定义锁对象
        Object obj = new Object();
        @Override
        public void run() {
            //模拟卖票
            while(true){
                //同步代码块
                synchronized (obj){
                    if (ticket > 0) {
                        //模拟电影选坐的操作
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
                    }
                }
            }
        }
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

同步方法
同步方法:在方法声明上加上synchronized

public synchronized void method(){
    //可能会产生线程安全问题的代码
}

同步方法中的锁对象是 this

使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:

package com.yw.web.demo3;
public class ThreadDemo {
    public static void main(String[] args) {
        //创建票对象
        Ticket ticket = new Ticket();
        //创建3个窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定义锁对象
    Object lock = new Object();
    @Override
    public void run() {
        //模拟卖票
        while(true){
            //同步方法
            method();
        }
    }
//同步方法,锁对象this
    public synchronized void method(){
        if (ticket > 0) {
            //模拟选坐的操作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
        }
    }
}

静态同步方法: 在方法声明上加上static synchronized

public static synchronized void method(){
//可能会产生线程安全问题的代码
}

静态同步方法中的锁对象是 类名.class

死锁问题

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

synchronzied(A锁){
    synchronized(B锁){

    }
}

代码演示:

package com.yw.web.demo3;
import java.util.Random;
class MyLock {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
}
class ThreadTask implements Runnable {
    int x = new Random().nextInt(1);//0,1
    //指定线程要执行的任务代码
    @Override
    public void run() {
        while(true){
            if (x%2 ==0) {
                //情况一
                synchronized (MyLock.lockA) {
                    System.out.println("if-LockA");
                    synchronized (MyLock.lockB) {
                        System.out.println("if-LockB");
                        System.out.println("if远望");
                    }
                }
            } else {
                //情况二
                synchronized (MyLock.lockB) {
                    System.out.println("else-LockB");
                    synchronized (MyLock.lockA) {
                        System.out.println("else-LockA");
                        System.out.println("else远望");
                    }
                }
            }
            x++;
        }
    }
}
public class ThreadDemoLock {
    public static void main(String[] args) {
        //创建线程任务类对象
        ThreadTask task = new ThreadTask();
        //创建两个线程
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        //启动线程
        t1.start();
        t2.start();
    }
}

Lock使用
查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的常用方法
这里写图片描述
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

package com.yw.web.demo3;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadDemo {
    public static void main(String[] args) {
        //创建票对象
        Ticket ticket = new Ticket();
        //创建3个窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //创建Lock锁对象
    Lock ck = new ReentrantLock();
    public void run() {
        //模拟卖票
        while(true){
            //synchronized (lock){
            ck.lock();
                if (ticket > 0) {
                    //模拟选坐的操作
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
                }
            ck.unlock();
        }
    }
}

Volatile使用
基础概念
可见性:
  可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线
程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他
线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
  在 Java 中 volatile、synchronized 和 final 实现可见性。

原子性:

  原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要
我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
有序性:
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
代码演示:

package com.yw.web.demo3;
/**
 * @author anyan
 */
public class NoVisibility {
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }
    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
 NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。甚至NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序”。只要在某个线程中无法检测到重排序情况(即使在其他线程中可以明显地看到该线程中的重排序),那么就无法确保线程中的操作将按照程序中指定的顺序来执行。当主线程首先写入number,然后在没有同步的情况下写入ready,那么读线程看到的顺序可能与写入的顺序完全相反。

  在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行进行判断,无法得到正确的结论。
  这个看上去像是一个失败的设计,但却能使JVM充分地利用现代多核处理器的强大性能。例如,在缺少同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中。此外,它还允许CPU对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中。
Volatile原理
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
这里写图片描述
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
  而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
当一个变量定义为 volatile 之后,将具备两种特性:
  1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
  2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile 性能:
  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

ThreadLocal使用
ThreadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的池子,池子中可以存储每个线程的私有数据。
其实就是thread的局部变量
ThreadLocal的方法有set()、get()、remove()、initialValue(),大家可以了解下
Get()与null
代码:

package com.yw.web.demo3;
public class Runn {
    @SuppressWarnings("rawtypes")
    public static ThreadLocal t1 = new ThreadLocal();
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        if (t1.get() == null) {
            System.out.println("从未放过值");
            t1.set("我的值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());
    }
}

运行结果:
这里写图片描述
控制台打印结果来看:第一次调用t1对象get()方法为null,set()后,才有值。ThreadLocal解决的是变量在不同线程间的隔离性。不同线程中的值是可以放入ThreadLocal类中进行保存的。
线程变量的隔离性
隔离性就是线程之间谁使用谁的局部变量
代码:

//工具类
package com.yw.web.demo3;

import java.util.Date;
public class Tools {
    public static ThreadLocal<Date> t1 = new ThreadLocal<Date>();

}
import java.util.Date;
public class Runnn {
    public static void main(String[] args) {
        try {
            ThreadA a = new ThreadA();
            a.start();
            Thread.sleep(1000);
            ThreadB b = new ThreadB();
            b.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class ThreadA extends Thread{
    @Override
    public void run() {
        try {
            for (int i = 0; i < 20; i++) {
                if (Tools.t1.get() ==null) {
                    Tools.t1.set(new Date());
                }
                System.out.println("A " + Tools.t1.get().getTime());
                Thread.sleep(100);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class ThreadB extends Thread{
    @Override
    public void run() {

        for (int i = 0; i < 20; i++) {
            if (Tools.t1.get() ==null) {
                Tools.t1.set(new Date());
            }
            System.err.println("B " + Tools.t1.get().getTime());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

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

A和B之间具有隔离性,各有各自所拥有的值。


4.单例模式与多线程
饿汉模式
饿汉模式说白了就是立即加载。
立即加载就是使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化。而立即加载有”着急”,”急迫”的含义,所以也成饿汉模式。
代码演示:

package com.yw.web.demo3;
class MyObject {
    //立即加载方式(饿汉模式)
    private static MyObject myObject = new MyObject();
    private MyObject() {
    }
    public static MyObject getInstance() {
        /*
         * 此代码版本为立即加载
         * 缺点:不能有其他实例变量,因为getInstance方法没有同步,可能存在线程不安全的问题
         *      
         */
        return myObject;
    }
}
class  MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:
这里写图片描述
控制台打印的hashCode是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。

懒汉模式
懒汉模式说白了就是延迟加载。
延迟加载就是在调用get()方法时实例才被创建,常见的实现方法就是在get()方法中进行new实例化。而延迟加载有”缓慢”,”不急迫”的含义,所以也成懒汉模式。

懒汉模式是在调用方法时实例才被创建。

存在的问题
代码演示:

package com.yw.web.demo3;
class MyObject {
    private static MyObject myObject;
    private MyObject() {
    }
    public static MyObject getInstance() {
        //延迟加载
        if(null != myObject){

        }else{
            try {
                Thread.sleep(3000);
                myObject = new MyObject();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return myObject;
    }
}
class  MyThread extends Thread {
    @Override
    public void run() {

        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

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

控制台打印的hashCode不是同一个值,说明对象不是同一个,也就说明了在多线程的环境中,就会出现取出多个实例的情况,与单例模式的初衷是相背离的。创建了“多例”的结果。

如何解决这一问题呢?
解决一:声明synchronized关键字
既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。
代码演示:

package com.yw.web.demo3;
class MyObject {
    private static MyObject myObject;
    private MyObject() {
    }
    synchronized public static MyObject getInstance() {
        //延迟加载
        if(null != myObject){   
        }else{
            try {
                Thread.sleep(3000);
                myObject = new MyObject();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return myObject;
    }
}
class  MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

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

此方法加入同步synchronized关键字得到相同实例的对象,但此种方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,则必须等上一个线程释放锁之后,才可以继续执行。

解决二:使用同步代码块

使用同步代码块配合DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式。

package com.yw.web.demo3;
class MyObject {
    private static MyObject myObject;
    private MyObject() {
    }
    //使用双检机制来解决问题,既解决了同步代码的异步执行性,又保证的了单例的效果
    public static MyObject getInstance() {
        try {// 延迟加载
            if (null != myObject) {
            } else {
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if (null == myObject) {
                        myObject = new MyObject();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:
这里写图片描述
使用这种方式,成功解决了懒汉模式遇到多线程的问题。

单例模式这里就介绍两种
注:很多知识点来源于:《Java多线程编程核心技术》一书 高洪岩编制
有兴趣的可以自学一下或者共同学习讨论。
提示:IDE工具玩多线程时,运行代码后若是死循环的代码尽量把控制台小红点关上。太耗资源**

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值