上帝视角学JAVA- 基础10-多线程【2021-08-08】

1、多线程

程序、进程、线程

  • 程序:完成特定任务、用某种语言编写的一组的指令的集合。即指的是一段静态的代码。

  • 进程:是程序的一次执行过程,或者是正在运行的一个程序。是一个动态的过程。有产生、存在、消亡的过程,即有生命周期

    进程作为资源分配的单位,系统在运行时为每个进程分配不同的内存区域。

  • 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。

    如果一个进程统一时间并行执行多个线程,就是支持多线程。

    线程作为调度和执行的单位,每个线程拥有独立的运行栈、程序计数器,线程切换的开销小

    一个进程内的多个线程共享相同的内存单元,它们从同一堆中分配对象,可以访问相同的变量和对象。这使得线程之间通信简单、高效。但是多个线程操作共享的系统资源可能会带来安全的隐患。

一个JAVA程序至少有三个线程,main主线程、gc垃圾回收线程、异常处理线程。

并行与并发:

并行:多个cpu同时执行多个任务。多个人同时做不同的事。

并发:一个cpu同时执行多个任务,如你一个人同时烧水、看电视、嗑瓜子。

1.1 线程的创建与使用

java.lang.Thread   // Thread类的全路径
    
public class Thread implements Runnable  // Thread 实现了Runnable 接口
​
// Runnable 接口的定义
@FunctionalInterface
public interface Runnable {
    
    public abstract void run();
}

Thread是 Runnable的一个实现类。实现了run方法。

使用Thread类来创建多线程。创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

// 继承 Thread 的子类
public class PrimeThread extends Thread{
​
    long minPrime;
​
    // 有参构造
    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }
    // 重写 run方法
    @Override
    public void run() {
    // 需要多线程执行的逻辑
    
    }
}
​
// 使用。先创建对象,使用对象点run
PrimeThread p = new PrimeThread(143);
p.start();

以上代码演示了创建多线程的一种方法。当执行了p.start()之后。就开启了一个新的线程。注意,p对象还是主线程创建的。

1.2 Thread 类有什么

Thread() 
          分配新的 Thread 对象。 
Thread(Runnable target) 
          分配新的 Thread 对象。 
Thread(Runnable target, String name) 
          分配新的 Thread 对象。 
Thread(String name) 
          分配新的 Thread 对象。 
Thread(ThreadGroup group, Runnable target) 
          分配新的 Thread 对象。 
Thread(ThreadGroup group, Runnable target, String name) 
          分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,并作为 group 所引用的线程组的一员。 
Thread(ThreadGroup group, Runnable target, String name, long stackSize) 
          分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,作为 group 所引用的线程组的一员,并具有指定的堆栈大小。 
Thread(ThreadGroup group, String name) 
          分配新的 Thread 对象。 
有这么8个重载的构造方法。
static int MAX_PRIORITY 
          线程可以具有的最高优先级。 
static int MIN_PRIORITY 
          线程可以具有的最低优先级。 
static int NORM_PRIORITY 
          分配给线程的默认优先级。 
有3个静态属性
有很多的方法
public static Thread currentThread()返回对当前正在执行的线程对象的引用。 
返回:当前执行的线程
    
public static void sleep(long millis)
                  throws InterruptedException
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。
    
public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。 
​
public final String getName()返回该线程的名称。 
等等很多的方法,这里就不写了。自己去看源码

1.3 Thread 应用

我们不能直接调用 Thread里面的run方法去启动线程。

start方法的使用告诉我们,不可以同一个对象start 2次(多次),即使这个线程结束了,也不能重新启动。只能是创新新的Thread对象,去start开启线程。

总结就是:首先创建一个类继承Thread类,里面重写run方法,run方法写需要异步进行的代码逻辑。然后创建对象,调用start方法。

我们发现,创建这个继承Thread类只用过一次,创建对象,并调用start。

我们可以用匿名类的方式来开启线程。

// 匿名类的匿名对象调用 start
new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 30; i++) {
                    System.out.println("哈哈哈"+i);
                }
            }
        }.start();

如何获取线程的名字呢?

使用Thread 里面的 getName()

// 这是使用的 匿名类的有名对象
Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    System.out.println("哈哈哈" + i);
                }
            }
        };
thread.start();
thread.getName();

使用 setName来设置线程名字

Thread thread = new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            System.out.println("哈哈哈" + i);
        }
    }
};
thread.start();
thread.setName("lalala");
thread.getName();

使用Thread的类方法【即静态方法】也可以获取当前线程的名字

Thread.currentThread().getName();

子类可以通过调用Thread构造器,创建对象时,就可以给线程命名。

public class PrimeThread extends Thread{
    // 构造器就设置线程名
    public PrimeThread(String threadName) {
        super(threadName);
    }
    public PrimeThread(){

    }
    // 重写 run方法
    @Override
    public void run() {
        System.out.println("run方法啊");
        for (int i = 0; i < 30; i++) {
            System.out.println("哈哈哈"+i);
        }
    }
}

yield 方法: 释放当前cpu的执行权,有可能下一刻又被分配到。

Thread 里面的 yield 方法定义:
public static native void yield();
是一个 静态的 native方法。
public class PrimeThread extends Thread{

    public PrimeThread(String threadName) {
        super(threadName);
    }
    public PrimeThread(){

    }
    // 重写 run方法
    @Override
    public void run() {
        System.out.println("run方法啊");
        for (int i = 0; i < 30; i++) {
            System.out.println("哈哈哈"+i);
            if ( i % 5 == 0) {
                // 不要使用 this.yield()  因为yiel的类方法,不应该使用 对象去调用
                yield();
            }
        }
    }
}

join 方法:在线程A中调用线程B的join方法,线程A将进入阻塞状态。直到线程B完全执行完以后才结束阻塞状态

例如主线程main 调用子线程【此时 main就是 A,子线程就是 B】,main 线程里面调用 子线程的 join方法,主线程就好阻塞直到子线程执行完全。

join就是加入的意思,即某一刻子线程加入到主线程里面,主线程要子线程加入,主线程就得等加入的线程执行完才能继续。

stop方法:强制停止线程。已弃用

sleep方法:就是让线程睡觉,不执行

public class PrimeThread extends Thread{
    // 重写 run方法
    @Override
    public void run() {
        System.out.println("run方法啊");
        for (int i = 0; i < 30; i++) {
            System.out.println(this.getName()+ ": 哈哈哈"+i);
            if (i % 2 == 0) {
                try {
                    // 单位:毫秒
                    sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意,在 PrimeThread 的run方法里面使用sleep 处理异常时只能 try catch,不能用throws。

因为子类重写方法抛出的异常不能大于父类的异常。父类没有抛异常,子类也不能抛异常。

isAlive方法: 判断当前线程是否还活着。

1.4 线程的优先级

cpu通过时间片的方式进行任务调用。

任务以抢占式获取cpu资源:高优先级的线程优先抢占资源。

static int MAX_PRIORITY = 10;
          线程可以具有的最高优先级。 
static int MIN_PRIORITY = 1;
          线程可以具有的最低优先级。 
static int NORM_PRIORITY = 5;
          分配给线程的默认优先级。 
// 获取优先级
public final int getPriority() {
return priority;
} 
// 设置优先级   要在start方法调用之前设置才有效
public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}  

注意:设置了线程优先级只能说是被执行的概率很高。不是一定就比其他线程先执行。

1.5 第二种创建多线程的方式

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。采用这种风格的同一个例子如下所示:

class PrimeRun implements Runnable {
    public void run() {
        // 多线程需要执行的逻辑
        . . .
    }
}

// 使用
PrimeRun p = new PrimeRun(143);
new Thread(p).start();

首先 创建一个实现 Runnable 接口的 实现类。并实现(重写)run方法。

然后实例化这个类的对象。将这个对象作为参数,传递给 实例化Thread对象的构造函数,再调用start方法。

其实,还是用的Thread类的对象进行调用start方法开启线程。

这2种方法区别是什么?

区别是 第一种创建方法是继承Thread类,重写run方法。实际上Thread类本身就是实现了Runnable接口的run方法。

我们继承Thread类再重写run方法,相当于我们实现了的是Runnable里面的run方法。

而第二种方式是我们直接实现 Runnable 里面的run方法。再把这个对象给Thread类。为什么要给Thread类?

因为Thread这个类才有start方法,才能开启线程。你要是不给Thread类,就需要在这个PrimeRun类里面也要有和Thread类一样的start方法。想要设置优先级,设置线程名字,sleep等等功能,都需要自己再写一遍。还不如直接给Thread类。

为什么给了Thread就可以了呢?

得看源码:

// 给Thread调用的是这个构造方法
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
这里执行了init方法,执行了一系列的初始化操作。
最终 调用start的时候执行的就是传进来的 Runnable 实现类里面的run方法。

为什么一定要重写run方法?

这就是废话,你不重写,我怎么知道开启了线程要干啥。

哪一种方式更好?

明显是第二种。因为用Thread子类的方式的话有很大的不便。因为这个类可能有自己的父类。要是继承了Thread类就不能继承自己的父类了。因为java是单继承的。而且只是为了重写一下run方法。完全可以用 实现接口的方式,即方式二来做多线程的事。

而且第二种方式,可以很方便的实现 多个线程共享数据。不需要加上static。只需要创建一个 实现类对象,多次给Thread,创建多个thread对象,这些thread对象执行的run方法都是一样的,数据也都是来源同一个实现类对象的。同一个对象当然数据共享。

1.6 线程的生命周期

// Thread 类里面有一个内部类叫做 State
public enum State 
NEW 新建状态
RUNNABLE 可运行状态【包括正在运行、等待运行 2种 状态】
BLOCKED 阻塞状态,等待锁 
WAITING 无限等待状态:无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。 
TIMED_WAITING 有限时间等待状态:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。 
TERMINATED 死亡状态:已退出的线程处于这种状态。 

线程一个定义了以上 6 种状态。必须要说明一点的是 是可运行状态,这个状态实际上是包括了2种状态。正在运行、等待cpu执行。

  • 创建一个Thread 对象就是新建一个线程。NEW状态

  • 调用start方法代表线程就绪,等待cpu执行。进入就绪状态 RUNNABLE ,cpu执行了,也是 RUNNABLE 状态。

  • 主动调用 yield 方法就是主动让cpu不执行了,处于 RUNNABLE 状态中的 就绪状态,会等待cpu分配资源,即使你调用了yield方法,退出当前cpu时间片,可能下一个时间片还是分配给你,即和没有调用yield效果一样。这是会发送的情况。 这种就绪与运行时都叫做 RUNNABLE 可运行状态。

  • 调用了 sleep 方法就处于 TIMED_WAITING 有限时间等待状态,等待需要睡觉的时间过去,自动回到 RUNNABLE 状态.

  • 调用 join 方法 就处于 WAITING 状态,无限期的等待别人执行完。别的线程执行完了,也会自动回到 RUNNABLE 状态

  • 调用 wait 方法进入到 WAITING 状态,直到手动调用 notify、notifyAll 方法 解除等待 ,回到 RUNNABLE 状态

  • 调用 suspend 挂起方法(已过时)同样会 进入 WAITING 状态,直到手动调用 resume (与suspend成对使用,同样也过时了)方法将回到 RUNNABLE 状态

  • 等待同步锁 会进入 BLOCKED 状态,直到获取了同步锁, JVM 自动退出 阻塞状态。回到 RUNNABLE 状态

  • RUNNABLE 状态的线程执行把代码全部执行完了,就进入TERMINATED死亡状态

  • TERMINATED 状态之前的任意状态,只要出现异常、错误且没有处理,就会直接进入 TERMINATED 死亡状态

 

1.7 线程安全问题

前面讲这个单例模式的懒汉式时,就提到过线程安全问题。原因是某些操作需要是原子性的。

以经典的卖票问题,分析这个线程安全问题。

public class TicketWindow implements Runnable{

    private int ticketTotal = 100;

    @Override
    public void run() {
        while (true){
            if (ticketTotal > 0) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + "卖票,票号为:"+ ticketTotal);
                ticketTotal--;
            }else{
                System.out.println(Thread.currentThread().getName() + "没票了卖不了。");
                break;
            }
        }
    }
}
// 测试
public class MainTest {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow();
        Thread thread1 = new Thread(ticketWindow);
        Thread thread2 = new Thread(ticketWindow);
        Thread thread3 = new Thread(ticketWindow);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

以上的代码模拟了3个窗口去卖票。卖票的时候模拟售票需要 200毫秒时间。

以下是某次运行结果:

Thread-0卖票,票号为:10
Thread-1卖票,票号为:10
Thread-2卖票,票号为:7
Thread-0卖票,票号为:7
Thread-1卖票,票号为:7
Thread-2卖票,票号为:4
Thread-0卖票,票号为:4
Thread-1卖票,票号为:4
Thread-1卖票,票号为:1
Thread-2卖票,票号为:1
Thread-2没票了卖不了。
Thread-0卖票,票号为:1
Thread-0没票了卖不了。
Thread-1没票了卖不了。

出现了卖同一种票的情况,也出现了某些票没有卖 如6号票,3号票等,也可能会出现 错误票的情况。即有线程安全问题。

原因就是 判断是否有票与扣减票数 这些操作应该是原子的。当一个线程在操作共享的数据 ticketTotal ,其他线程不能参与。

操作完成后,其他线程才能去操作这个共享数据。

操作共享数据的过程包括 判断票数,输出语句,扣减票数。这是 1整个过程。

1.7.1 解决方案1:同步代码块

同步代码块

 synchronized(同步监视器){
    // 需要被同步的代码
 }

什么是需要同步的代码?操作共享数据的代码。即需要变成一个原子性操作的代码。

什么是同步监视器?

就是锁。任何一个类的对象,都可以当成锁。但是要求多个线程必须要共用同一把锁。

public class TicketWindow implements Runnable{

    private int ticketTotal = 100;
    // 这里就以 Object 对象作为 锁
    private Object obj = new Object();

    @Override
    public void run() {
        while (true){
           synchronized (obj){
               if (ticketTotal > 0) {
                   System.out.println(Thread.currentThread().getName() + "卖票,票号为:"+ ticketTotal);
                   try {
                       Thread.sleep(200);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   ticketTotal--;
               }else{
                   System.out.println(Thread.currentThread().getName() + "没票了卖不了。");
                   break;
               }
           }
        }
    }
}

// 测试
public class MainTest {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow();
        Thread thread1 = new Thread(ticketWindow);
        Thread thread2 = new Thread(ticketWindow);
        Thread thread3 = new Thread(ticketWindow);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

这里面 锁 private Object obj = new Object(); 是成员变量。属于对象的。在测试的时候,只new了一个对象 ticketWindow,所以,这个锁只有1份。开启的三个线程都是共用同一把锁。

注意下面的代码:

public class TicketWindow implements Runnable {

    private int ticketTotal = 100;
    
    @Override
    public void run() {
        while (true) {
            // 充当锁的对象在这里创建。
            Object obj = new Object();
            synchronized (obj) {
                if (ticketTotal > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticketTotal);
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticketTotal--;
                } else {
                    System.out.println(Thread.currentThread().getName() + "没票了卖不了。");
                    break;
                }
            }
        }
    }
}

上面的这段代码中的锁就没有用。因为放在了 run方法里面。是局部变量。每个线程执行run方法都会创建自己的 锁。不满足所有线程共用同一把锁的要求。

同步代码块的方式解决了线程安全的问题。但是这一段代码就相当于是单线程执行了。

new 一个对象出来当锁还是太麻烦了。类里面就有对象。

public class TicketWindow implements Runnable {

    private int ticketTotal = 100;

    @Override
    public void run() {
        while (true) {
            // 直接用 this 即当前对象充当 锁。只要保证 是同一个对象,锁就是相同的。
            synchronized (this) {
                if (ticketTotal > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticketTotal);
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticketTotal--;
                } else {
                    System.out.println(Thread.currentThread().getName() + "没票了卖不了。");
                    break;
                }
            }
        }
    }
}

还是麻烦,要保证 是同一个对象。

// 直接用当前类去充当 锁。
synchronized (TicketWindow.class) {
}

锁应该是一个对象,这里是用的类。这说明类本身也是对象。

类也是对象,你就明白了为什么可以通过类名点的方式调用静态属性或者方法。因为这也是 对象点的方式 调用。

类只会加载一次,说明TicketWindow.class类对象也只会有1个。

综上,还是用 类名.class 来充当锁比较适合我这种懒人。

1.7.2 解决方案2:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们就可以声明这个方法是同步的。

如果不是在一个方法里,我们可以改造代码,创建一个这个的方法就是了。【没有条件,创造条件】

public class TicketWindow implements Runnable {

    private int ticketTotal = 100;

    @Override
    public void run() {
        do {
            sell();
        } while (ticketTotal > 0);
    }
    // 这个同步锁就是 this
    private synchronized void sell(){
        if (ticketTotal > 0) {
            System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticketTotal);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketTotal--;
        } else {
            System.out.println(Thread.currentThread().getName() + "没票了卖不了。");
        }
    }
}

sell 方法就是我们改造的方法。将需要同步的逻辑抽取成一个方法,用synchronized 修饰一下即可。注意,这种方式依然是有锁的,这个锁就是 this。

如果采用 继承Thread类的方式来实现 多线程,这个this就不一定是能保证是同一把锁了。我们可以考虑把这个抽取的方法定义为static。

还记得static吗?static的左右就是归属权升级。变成类的。这个时候这个默认的锁就不this了,而是类对象。

当然,实现Runnable 的方式也可以这样升级。

public class TicketWindow implements Runnable {

    private static int ticketTotal = 100;

    @Override
    public void run() {
        do {
            // 非静态方法run是可以调用静态方法、静态变量的
            sell();
        } while (ticketTotal > 0);
    }
    // 这个同步锁在非静态方法是 this, 如果是静态方法就是 当前类对象
    private static synchronized void sell(){
        if (ticketTotal > 0) {
            System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticketTotal);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketTotal--;
        } else {
            System.out.println(Thread.currentThread().getName() + "没票了卖不了。");
        }
    }
}

1.7.3 解决方案3:同步锁

JDK 5.0 开始 JAVA提供了更强大的线程同步机制。显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

Lock 是一个接口。它有一个实现类ReentrantLock,拥有与synchronized相同的并发性和内存语义,在线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。

选择:优先用lock、再次同步代码块、最后是同步方法。

强大在哪里呢?可以显示的加锁、释放锁了。前面的同步代码块、同步方法都是只能在一个地方加锁,释放锁都是自动的。无法精确控制锁的加锁与释放。

public class TicketWindow3 implements Runnable {
    private static int ticketTotal = 100;

    // Lock 的一个实现类,可以设置 是否公平锁。
    private ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        while (true) {
            try {
                // 加锁
                lock.lock();
                if (ticketTotal > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticketTotal);
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticketTotal--;
                } else {
                    System.out.println("卖完了,没票了");
                    break;
                }
            } finally {
                // 无论是否有异常,都会解锁
                lock.unlock();
            }
        }
    }
}

// 测试
public class MainTest {
    public static void main(String[] args) {
        TicketWindow3 ticketWindow = new TicketWindow3();
        Thread thread1 = new Thread(ticketWindow);
        Thread thread2 = new Thread(ticketWindow);
        Thread thread3 = new Thread(ticketWindow);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

放在try finally 里面,发生了异常也会解锁。

1.8 解决单例-懒汉式的线程安全问题

原始的 懒汉式代码:

// 单例实现方式2:懒汉式
public class SingletonMode2 {
    
    // 私有的、静态的 SingletonMode2 类型的 变量 存储 对象, 默认初始化。
    private static SingletonMode2 instance;

    // 公有的、静态的 返回值为 SingletonMode2 类型的 方法 给外界提供对象
    public static SingletonMode2 getInstance() {
        // 如果 instance 是null 就创建对象,并赋值给instance,如果不是null,就返回给外界
        if (Objects.isNull(instance)) {
            instance = new SingletonMode2();
        }
        return  instance;
    }
    
    private SingletonMode2(){
        
    }
}

@Test
public void test1(){
    SingletonMode2 instance = SingletonMode2.getInstance();
}

改进的懒汉式代码:

public class SingletonMode2Improve {

    // 私有的、静态的 SingletonMode2 类型的 变量 存储 对象, 默认初始化。
    private static SingletonMode2Improve instance;

    // 公有的、静态的 返回值为 SingletonMode2 类型的 方法 给外界提供对象
    public static synchronized SingletonMode2Improve getInstance() {
        // 如果 instance 是null 就创建对象,并赋值给instance,如果不是null,就返回给外界
        if (Objects.isNull(instance)) {
            instance = new SingletonMode2Improve();
        }
        return instance;
    }

    private SingletonMode2Improve() {

    }
}

上面是最简单的实现。因为需要同步的代码正好是一个方法。而且是静态的,锁是

SingletonMode2Improve.class

也可以用同步代码块的方式:

public class SingletonMode2Improve {
    
    private static SingletonMode2Improve instance;

    public static SingletonMode2Improve getInstance() {
        
       synchronized (SingletonMode2Improve.class){
           if (Objects.isNull(instance)) {
               instance = new SingletonMode2Improve();
           }
           return instance;
       }
    }

    private SingletonMode2Improve() {

    }
}

上面的代码效率稍微差了一点。还可以改进。因为我们真正需要同步的代码是 判断 是否有对象与创建对象。

public class SingletonMode2Improve {

    private static SingletonMode2Improve instance;

    public static SingletonMode2Improve getInstance() {
        // 多加一层判断,如果存在直接返回,不需要进入 同步代码块。
        if (Objects.isNull(instance)) {
            synchronized (SingletonMode2Improve.class) {
                if (Objects.isNull(instance)) {
                    instance = new SingletonMode2Improve();
                }
            }
        }
        return instance;
    }

    private SingletonMode2Improve() {

    }
}

上面的改进就是 只针对 对象不存在的情况创建对象进行同步。存在的情况直接返回。

1.9 死锁问题

什么是死锁?

不同的线程分别占用了对方需要的同步资源不放,都在等待对方释放自己需要的同步资源,就形成了线程的死锁。

你依赖我完成才能解锁,我也依赖你完成才能解锁。就是死锁了。

比如: 你和你老婆吵架了,你说你先道歉我马上就道歉,你老婆也说你先道歉我然后再道歉。然后就死锁了。

都在等对方道歉,冷战就离婚了。。。。太可怕了。

出现死锁后,不会出现异常,不会有提示,所有线程都处于WAITING或者BLOCKED状态。

看如下代码:

1.9.1 解决方案

  • 专门的算法、原则

  • 尽量减少同步资源的定义

  • 尽量避免嵌套同步

public class DeadLockTest {
    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        // Thread 匿名子类方式
        new Thread(){
            @Override
            public void run(){
                synchronized(s1){
                    s1.append("a");
                    s2.append("1");
                  
                    // 嵌套的 同步锁
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        // Runnable 匿名实现类方式
        new Thread(new Runnable(){

            @Override
            public void run() {
                synchronized(s2){
                    s1.append("c");
                    s2.append("3");
                    
                    // 嵌套的 同步锁
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

上面的代码可能出现死锁,概率较低,因为执行的很快。

模拟线程执行慢一点。死锁概率大大增加。

public class DeadLockTest {
    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        // Thread 匿名子类方式
        new Thread(){
            @Override
            public void run(){
                synchronized(s1){
                    s1.append("a");
                    s2.append("1");
                    // 模拟执行业务需要100毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 嵌套的 同步锁
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        // Runnable 匿名实现类方式
        new Thread(new Runnable(){

            @Override
            public void run() {
                synchronized(s2){
                    s1.append("c");
                    s2.append("3");
                    // 模拟执行业务需要100毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 嵌套的 同步锁
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

增加了Thread.sleep后,很容易出现死锁。不会报错,但是执行不下去。

上面的代码就是使用了同步代码块嵌套使用。很容易出现死锁。

1.10 线程的通信

例如交替打印1到100个数

public class ThreadCommunicat implements Runnable{
    private int num = 1;

    @Override
    public void run() {
        while (true){
            synchronized (ThreadCommunicat.class){
                // 唤醒1个线程
                ThreadCommunicat.class.notify();
               if (num<=100){
                   try {
                       Thread.sleep(10);
                   }catch (InterruptedException e){
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName()+": "+ num);
                   num++;
                   try {
                       // 调用wait使得当前线程进入无限等待状态,直到被notify或者notifyAll 唤醒
                       ThreadCommunicat.class.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }else {
                   break;
               }
            }
        }
    }
}

这里使用了 wait、notify/notifyAll 方法。这3个方法的调用者必须是同步监视器,即锁对象。如果不是同一个对象,就会报错java.lang.IllegalMonitorStateException!

这三个方法是定义在Object中。因为任意对象都可以作为同步监视器。任意对象都需要可以调用这三个方法。这就是任意对象的公共方法。所以定义在Object类中。

ThreadCommunicat.class 这个玩意也是个对象,也继承了Object。当然可以调用Object里面的方法。

1.11 sleep 与 wait 的异同

wait 必须被 notify或者notifyAll 唤醒,没有被唤醒就是无限等待。

sleep必须定义时间,时间到了会自动唤醒,这个就是有限时间等待。

sleep方法是定义在Thread类里面,而wait是在Object里面。

调用wait、notify/notifyAll 三个方法的对象要求必须是同一个同步监视器对象,所以wait只能用在同步代码块或者同步方法中。

sleep 不会释放锁,而wait会释放锁。

1.12 JDK5 新增的2个创建线程的方式

1.12.1 实现Callable接口

新增的Callable 接口功能更加强大。

相比 Runable接口只有一个无返回值的 run 方法且不能抛出异常,Callable更加强大。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

call 方法 支持了返回值也可以抛出异常。

需要借助FutureTask 类,比如获取结果。

public class PrimeCall implements Callable {

    @Override
    public Object call() throws Exception {
        // 干点活
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
            }
            sum += i;
        }
        // 返回值
        return sum;
    }
}

Future 接口:

public interface Future<V> {
	// 取消
    boolean cancel(boolean mayInterruptIfRunning);
	// 是否已取消
    boolean isCancelled();
	// 是否完成
    boolean isDone();
	// 获取结果
    V get() throws InterruptedException, ExecutionException;
	// 获取结果
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask 是 Future的唯一实现类。 同时实现了 Runnable、Future接口。既可以作为Runnable被线程执行,又可以作为Future 得到Callable的返回值。

 

FutureTask 类有2个构造器,没有无参构造器。一个是传入 Callable实现类对象,一个是Runnable 实现类对象和 另一个对象。

public class MainTest {
    public static void main(String[] args) {
        // new Callable 实现类对象
        PrimeCall call = new PrimeCall();
         // Callable 实现类对象call 给FutureTask ,得到 futureTask 对象
        FutureTask futureTask = new FutureTask(call);
        // futureTask给Thread ,得到Thread对象 调用start 启动线程。
        new Thread(futureTask).start();
        try {
            // futureTask 可以获取返回值
            Object o = futureTask.get();
            System.out.println("返回值是:"+o);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

上面的代码是如何使用。首先是new Callable的实现类对象给FutureTask得到futureTask对象,再将futureTask对象给Thread,通过Thread启动线程。

futureTask可以调用get获取返回值。

你会发现最终还是要Thread参与啊。因为启动线程的方法定义在了Thread类里面。

PrimeCall 类继承 Call类的时候 是可以指定返回值泛型的。等学习到泛型再说。

1.12.2 使用线程池

JDK5.0 提供的新特性 通过线程池创建多线程。

使用线程池的好处:

  • 便于管理资源

  • 提高响应速度、降低资源(线程池的线程不需要每次都创建,可以重复利用)

提取创建多个线程,放到一个池子里面,需要用的时候就来取。用完放回。

线程池涉及到2个Api:ExecutorService接口、Executors工具类

public class PrimeExecutor implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}
public class MainTest {
    public static void main(String[] args) {
        // 创建一个固定线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        // 看一下 service是那个类的对象  class java.util.concurrent.ThreadPoolExecutor
        System.out.println(service.getClass());
        // 适合Runnable
        service.execute(new PrimeExecutor());
        // 适合Callable
        service.submit(new PrimeCall());
        // 关闭线程池
        service.shutdown();
    }
}

execute需要一个Runnable 实现类对象。

submit需要Callable实现类对象。

原因是池子虽然造好了,但是要干啥,你不是给传给我。重写run方法或者call方法,就是在写要干什么。

public class MainTest {
    public static void main(String[] args) {
        // 创建一个固定线程池
        ThreadPoolExecutor service = (ThreadPoolExecutor)Executors.newFixedThreadPool(10);
        // 设置 核心线程大小
        service.setCorePoolSize(20);
        // 设置存活时间
        service.setKeepAliveTime(100, TimeUnit.MICROSECONDS);
        System.out.println(service.getClass());
        // 适合Runnable
        service.execute(new PrimeExecutor());
        // 适合Callable
        service.submit(new PrimeCall());
        // 关闭线程池
        service.shutdown();
    }
}

上面的代码表示可以对线程池进行设置。

还没有讲 线程池具体参数的含义,异步编排等知识。有机会再补充!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值