Java学习总结(2021版)---多线程

一:基本概念:程序、进程、线程

  • 程序可以理解为静态的代码
  • 进程可以理解为执行中的程序
  • 线程可以理解为进程的进一步细分,程序的一条执行路径

使用多线程的优点:
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

二:线程的创建和使用

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。

方式一:继承于Thread类

  • 1.创建一个继承于Thread类的子类
  • 2.重写Thread类的run() --> 将此线程执行的操作声明在run()中
  • 3.创建Thread类的子类的对象
  • 4.通过此对象调用start()
 例子:遍历100以内的所有的偶数

//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
    
    
    //2. 重写Thread类的run()
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i % 2 == 0){
    
    
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}


public class ThreadTest {
    
    
    public static void main(String[] args) {
    
    
        //3. 创建Thread类的子类的对象
        MyThread t1 = new MyThread();
        //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
        t1.start();
        //问题一:我们不能通过直接调用run()的方式启动线程。
//       t1.run();
        //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
//        t1.start();
        //我们需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();      
    }

}


// 通过继承Thread类来创建线程类
public class MyThreadTest extends Thread {
    
    
    private int i;
    // 重写run方法,run方法的方法体就是线程执行体
    public void run() {
    
    
        for (; i < 100; i++) {
    
    
            // 当线程类继承Thread类时,直接使用this即可获取当前线程
            // Thread对象的getName()返回当前该线程的名字
            // 因此可以直接调用getName()方法返回当前线程的名
            System.out.println(getName() + "" + i);
        }
    }
    public static void main(String[] args) {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            // 调用Thread的currentThread方法获取当前线程
            System.out.println(Thread.currentThread().getName() + "" + i);
            if (i == 20) {
    
    
                // 创建、并启动第一条线程
                new MyThreadTest().start();
                // 创建、并启动第二条线程
                new MyThreadTest().start();
            }
        }
    }
}


在这里插入图片描述虽然上面程序只显式地创建并启动了2个线程,但实际上程序有3个线程,即程序显式创建的2个子线程和1个主线程。前面已经提到,当Java程序开始运行后,程序至少会创建一个主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的,main()方法的方法体代表主线程的线程执行体。

该程序无论被执行多少次输出的记录数是一定的,一共是300条记录。主线程会执行for循环打印100条记录,两个子线程分别打印100条记录,一共300条记录。因为i变量是MyThreadTest的实例属性,而不是局部变量,但因为程序每次创建线程对象时都需要创建一个MyThreadTest对象,所以Thread-0和Thread-1不能共享该实例属性,所以每个线程都将执行100次循环。

两个问题:

  • 问题一:我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
  • 问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().

注意点:

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU
    调度决定。
  3. 想要启动多线程,必须调用start方法。
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上
    的异常“IllegalThreadStateException”。
    练习:
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数

public class ThreadDemo {
    
    
    public static void main(String[] args) {
    
    
    	MyThread1 m1 = new MyThread1();
  	  	MyThread2 m2 = new MyThread2();
    	m1.start();
   	 	m2.start();

      
class MyThread1 extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i % 2 == 0){
    
    
                System.out.println(Thread.currentThread().getName() + ":" + i);

            }
        }

    }
}


class MyThread2 extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i % 2 != 0){
    
    
                System.out.println(Thread.currentThread().getName() + ":" + i);

            }
        }

    }
}

方式二:实现Runnable接口

  • 1.创建一个实现了Runnable接口的类
  • 2.实现类去实现Runnable中的抽象方法:run()
  • 3.创建实现类的对象
  • 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  • 5.通过Thread类的对象调用start()
//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
    
    

    //2. 实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i % 2 == 0){
    
    
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

        }
    }
}


public class ThreadTest1 {
    
    
    public static void main(String[] args) {
    
    
        //3. 创建实现类的对象
        MThread mThread = new MThread();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        t1.setName("线程1");
        //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(mThread);
        t2.setName("线程2");
        t2.start();
    }

}

public class MyRunnableTest implements Runnable {
    
    
    private int i;
    void print(){
    
    
         System.out.println(Thread.currentThread().getName() + "" + i);
    }
    // run方法同样是线程执行体
    public void run() {
    
    
        for (; i < 100; i++) {
    
    
            // 当线程类实现Runnable接口时,
            // 如果想获取当前线程,只能用Thread.currentThread()方法。
            print();
        }
    }
    public static void main(String[] args) {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println(Thread.currentThread().getName() + "" + i);
            if (i == 20) {
    
    
                MyRunnableTest st = new MyRunnableTest();
                // 通过new Thread(target , name)方法创建新线程
                new Thread(st, "新线程-1").start();
                new Thread(st, "新线程-2").start();
            }
        }
    }
}

在这里插入图片描述从该运行结果中我们可以看出,控制台上输出的内容是乱序的,而且每次结果不尽相同。这是因为:

  1. 在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target。
  2. 所以多个线程可以共享同一个线程类即线程的target类的实例属性。
  3. 往控制台窗口print()输出的过程并不是多线程安全的,在一个线程输出过程中另一个线程也可以输出。

为能够保证顺序输出,我们可以对打印方法设置Synchronized,让每次只能有一个进程能够访问打印,代码如下:

public class MyRunnableTest implements Runnable {
    
    
    private int i;
    synchronized void print(){
    
    
         System.out.println(Thread.currentThread().getName() + "" + i);
    }
    // run方法同样是线程执行体
    public void run() {
    
    
        for (; i < 100; i++) {
    
    
            // 当线程类实现Runnable接口时,
            // 如果想获取当前线程,只能用Thread.currentThread()方法。
            print();
        }
    }
    public static void main(String[] args) {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println(Thread.currentThread().getName() + "" + i);
            if (i == 20) {
    
    
                MyRunnableTest st = new MyRunnableTest();
                // 通过new Thread(target , name)方法创建新线程
                new Thread(st, "新线程-1").start();
                new Thread(st, "新线程-2").start();
            }
        }
    }
}

在这里插入图片描述

比较创建线程的两种方式

  • 开发中:优先选择:实现Runnable接口的方式
  • 原因:
    • 1.实现的方式没有类的单继承性的局限性
    • 2.实现的方式更适合来处理多个线程有共享数据的情况。
  • 联系:public class Thread implements Runnable
  • 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

方式三:JDK 5.0新增线程创建方式–实现Callable接口

实现方法:

  1. 创建一个实现Callable的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()中
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  6. 获取Callable中call方法的返回值
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
    
    
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
    
    
        int sum = 0;
        for(int i = 1;i<=10;i++){
    
    
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}
public class ThreadNew {
    
    
    public static void main(String[] args){
    
    
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
        //FutureTask 同时实现了Runnable, Future接口
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
        new Thread(futureTask).start();
        try {
    
    
            //6.获取Callable中call方法的返回值,不需要返回值可以不调
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
        }
    }
}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

  • call()可以返回值的。
  • call()可以抛出异常,被外面的操作捕获,获取异常的信息
  • Callable是支持泛型的

Future接口概述

Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口和Runnable接口可以作为Thread类的target。在Future接口里定义了如下几个公共方法来控制它关联的Callable任务

  • 1)boolcan cancel(boolean maylnterruptltRunning):试图取消该Future里关联的Callable任务
  • 2)V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值
  • 3)V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值, 将会抛出TimeoutExccption异常
  • 4)boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true
  • 5)boolean isDone():妇果Callable任务已完成,则返回true
    注意:Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同。

方式四:JDK 5.0新增线程创建方式–使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程对性能影响很大。

解决方案:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

实现方法:

  • 提供指定线程数量的线程池
  • 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
  • 关闭连接池

相关API:

JDK 5.0起提供了线程池相关AP|: Executor Service和 Executors

Executor Service:真正的线程池接口。常见子类 Thread Poolexecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T>task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors. newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFⅸedthreadPool(n);创建一个可重用固定线程数的线程池
EXecutors. newSingleThreadEXecutor():创建一个只有一个线程的线程池
Executors. new thread Poo(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
class NumberThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for(int i = 0;i<10;i++){
    
    
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

class Number2Thread implements Callable {
    
    
    @Override
    public Object call() throws Exception {
    
    
        int sum = 0;
        for(int i = 1;i<=10;i++){
    
    
            System.out.println(Thread.currentThread().getName()+":"+i);
            sum+=i;
        }
        return sum;
    }
}

public class ThreadPool {
    
    
    public static void main(String[] args) {
    
    
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);//创建一个可重用固定线程数为10的线程池

        //查看该对象是哪个类造的
        System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
        //设置线程池的属性
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        Future future = service.submit(new Number2Thread());//适合使用于Callable
        try {
    
    
            System.out.println(future.get());//输出返回值
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }
}

应用线程池的好处:

  • 1.提高响应速度(减少了创建新线程的时间)
  • 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 3.便于线程管理
    corePoolSize:核心池的大小
    maximumPoolSize:最大线程数
    keepAliveTime:线程没任务时最多保持多长时间后会终止

测试Thread中的常用方法:

方法 描述
public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法
public void run() 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
public final void setName(String name) 改变线程名称,使之与参数 name 相同
public Thread(String name) Thread的有参构造方法 ,创建线程的时候设置名字
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec) 让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
public final void join() 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
public final void join(long millisec) 等待该线程阻塞的时间最长为 millis 毫秒。
public final boolean isAlive() 测试线程是否处于活动状态。
public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public void interrupt() 中断线程。
public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。
public final void setPriority(int priority) 更改线程的优先级
public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。

部分方法测试:

1:currentThread()、getName()和setName()
在这里插入图片描述
2:Thread(String name)和yield()
调用 yield 会让当前线程从 Running 进入 Runnable 状态,即让出 cpu 的使用权, 与其他同样是 Runnable 状态的线程一齐竞争 cpu 的使用权。

class HelloThread extends Thread{
    
    
    public HelloThread(String name) {
    
    
        super(name);
    }
    public HelloThread(){
    
    
    }
    @Override
    public void run(){
    
    
        for(int i = 0;i<=5;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

public class ThreadMethodTest {
    
    
    public static void main(String[] args) {
    
    
        //创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
        HelloThread helloThread = new HelloThread("线程一");
        helloThread.start();
        for(int i = 0;i<=5;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"---"+i);
            if(i == 2){
    
    
                // 当i为2时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
                Thread.yield();
            }
        }
    }
}

在这里插入图片描述
3:sleep()
调用 Thread.sleep() 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),睡眠结束线程恢复执行(但是不一定会立刻执行)

class HelloThread extends Thread{
    
    
    public HelloThread(String name) {
    
    
        super(name);
    }
    public HelloThread(){
    
    
    }
    @Override
    public void run(){
    
    
        for(int i = 0;i<=3;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

public class ThreadMethodTest {
    
    
    public static void main(String[] args) {
    
    
        //创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
        HelloThread helloThread = new HelloThread("线程一");
        helloThread.start();
        for(int i = 0;i<=3;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"---"+i);
            try {
    
    
                //设置进入1000毫秒阻塞状态
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述
4:join()
在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。

class HelloThread extends Thread{
    
    
    public HelloThread(String name) {
    
    
        super(name);
    }
    public HelloThread(){
    
    
    }
    @Override
    public void run(){
    
    
        for(int i = 0;i<=3;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

public class ThreadMethodTest {
    
    
    public static void main(String[] args) {
    
    
        //创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
        HelloThread helloThread = new HelloThread("线程一");
        helloThread.start();
        for(int i = 0;i<=3;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"---"+i);
            if(i == 2){
    
    
                try {
    
    
                    //此时主线程等待线程一的计算完成,主线程再运行
                    helloThread.join();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
}

在这里插入图片描述
5:isAlive()
测试线程是否处于活动状态。返回false、true

class HelloThread extends Thread{
    
    
    public HelloThread(String name) {
    
    
        super(name);
    }
    public HelloThread(){
    
    
    }
    @Override
    public void run(){
    
    
        System.out.println(Thread.currentThread().getName()+"正在执行");
    }
}

public class ThreadMethodTest {
    
    
    public static void main(String[] args) {
    
    
        //创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
        HelloThread helloThread = new HelloThread("线程一");
        helloThread.start();
        System.out.println(helloThread.getName()+"的状态:"+helloThread.isAlive());
        System.out.println(Thread.currentThread().getName()+"正在执行");
        try {
    
    
            //helloThread线程开始执行,主线程等待
            helloThread.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //此时线程一的已经执行完,状态已经死亡
        System.out.println(helloThread.getName()+"的状态:"+helloThread.isAlive());
        System.out.println(Thread.currentThread().getName()+"的状态:"+Thread.currentThread().isAlive());
    }
}

在这里插入图片描述

线程的优先级

1:等级

  • MAX_PRIORITY:10
  • MIN _PRIORITY:1
  • NORM_PRIORITY:5 -->默认优先级

2:如何获取和设置当前线程的优先级:

  • getPriority():获取线程的优先级
  • setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

class HelloThread extends Thread{
    
    
    public HelloThread(String name) {
    
    
        super(name);
    }
    public HelloThread(){
    
    
    }
    @Override
    public void run(){
    
    
        System.out.println(Thread.currentThread().getName()+"正在执行");
    }
}

public class ThreadMethodTest {
    
    
    public static void main(String[] args) {
    
    
        //创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
        HelloThread helloThread = new HelloThread("分线程");
        //默认分线程和主线程的优先级都是5
        System.out.println(helloThread.getName()+"的优先级:"+helloThread.getPriority());
        System.out.println(Thread.currentThread().getName()+"的优先级:"+Thread.currentThread().getPriority());
        helloThread.setPriority(Thread.MAX_PRIORITY);
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        System.out.println("-----");
        //设置分线程和主线程的优先级
        System.out.println(helloThread.getName()+"的优先级:"+helloThread.getPriority());
        System.out.println(Thread.currentThread().getName()+"的优先级:"+Thread.currentThread().getPriority());

        helloThread.start();
        System.out.println(Thread.currentThread().getName()+"正在执行");
    }
}

在这里插入图片描述

三:线程生命周期

在这里插入图片描述在这里插入图片描述

四:线程的同步

  • 为什么要使用同步?看下面这个例子:
/**
 * 例子:创建三个窗口卖票,总票数为10张.使用实现Runnable接口的方式
 */
class WindowThread implements Runnable{
    
    
    private int ticket = 10;
    @Override
    public void run() {
    
    
        while(true){
    
    
            if(ticket > 0){
    
    
                System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
                ticket--;
            }else{
    
    
                break;
            }
        }
    }
}
public class WindowTest2 {
    
    
    public static void main(String[] args) {
    
    
        WindowThread windowThread = new WindowThread();
        Thread t1 = new Thread(windowThread);
        Thread t2 = new Thread(windowThread);
        Thread t3 = new Thread(windowThread);
        t1.start();
        t2.start();
        t3.start();
    }
}

在这里插入图片描述

1、线程安全问题存在的原因:

由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。

2、如何解决线程安全问题

必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。

3、java如何实现线程安全:线程的同步机制

方式一:同步代码块

synchronized(同步监视器){
    
    
//需要被同步的代码块(即为操作共享数据的代码)
}

1、共享数据:多个线程共同操作的同一个数据(变量)
2、同步监视器:由任何一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁。
要求:多个线程必须要共用同一把锁。
3、在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器
4:、在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器

一:使用同步代码块解决实现Runnable接口的线程安全问题

class Window2 implements Runnable {
    
    
    int ticket = 100;// 共享数据
    
    public void run() {
    
    
        while (true) {
    
    
            synchronized (this) {
    
    //this表示当前对象,本题中即为w
                if (ticket > 0) {
    
    
                    try {
    
    
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
    
    
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "售票,票号为:" + ticket--);
                }
            }
        }
    }
}

public class TestWindow2 {
    
    
    public static void main(String[] args) {
    
    
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
class Animal{
    
    

}

二:使用同步代码块解决继承Thread类的方式的线程安全问题

二:使用同步代码块解决继承Thread类的方式的线程安全问题
 * 例子:创建三个窗口卖票,总票数为100.使用继承Thread类的方式
 * 说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。

class Window2 extends Thread{
    
    

    private static int ticket = 100;

    private static Object obj = new Object();

    @Override
    public void run() {
    
    

        while(true){
    
    
            //正确的
//            synchronized (obj){
    
    
            synchronized (Window2.class){
    
    //Class clazz = Window2.class,Window2.class只会加载一次
                //错误的方式:this代表着t1,t2,t3三个对象
//              synchronized (this){
    
    

                if(ticket > 0){
    
    

                    try {
    
    
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }

                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                }else{
    
    
                    break;
                }
            }

        }

    }
}


public class WindowTest2 {
    
    
    public static void main(String[] args) {
    
    
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();


        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,可以将其定义为同步方法。
例如:

public synchronized void show (String name){
    
    .
}

一:使用同步方法解决实现Runnable接口的线程安全问题


一:使用同步方法解决实现Runnable接口的线程安全问题
 *  关于同步方法的总结:
 *  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
 *  2. 非静态的同步方法,同步监视器是:this
 *     静态的同步方法,同步监视器是:当前类本身

class WindowThread3 implements Runnable{
    
    
    private int ticket = 10;
    @Override
    public void run() {
    
    
        while(true){
    
    
            show();
        }
    }
    //同步show方法,继承Thread类方法一样,只需同步方法即可,同时需要给方法加static关键字,确保不会创建多个对象
    private synchronized void show(){
    
    //同步监视器默认为this
        if(ticket > 0){
    
    
            System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
            ticket--;
        }
    }
}
public class WindowTest3 {
    
    
    public static void main(String[] args) {
    
    
        WindowThread3 windowThread3 = new WindowThread3();
        Thread t1 = new Thread(windowThread3);
        Thread t2 = new Thread(windowThread3);
        Thread t3 = new Thread(windowThread3);
        t1.start();
        t2.start();
        t3.start();
    }
}

在这里插入图片描述

二:使用同步方法处理继承Thread类的方式中的线程安全问题

class Window4 extends Thread{
    
    
    private static int ticket = 10;
    @Override
    public void run() {
    
    
        //买票操作
        while (true) {
    
    
             show();
        }
    }
    //生命成静态synchronized方法
    private static synchronized void show(){
    
    //同步监视器:Window4.class
        //private synchronized void show(){ //同步监视器不唯一。此种解决方式是错误的
        if (ticket > 0) {
    
    
            System.out.println(Thread.currentThread().getName() + ":买票,票号为:" + ticket);
            ticket--;
        }
    }
}

public class WindowTest4 {
    
    
    public static void main(String[] args) {
    
    
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Window4 w3 = new Window4();
        w1.start();
        w2.start();
        w3.start();
    }
}

方式三:Lock锁 — JDK 5.0新增

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与 synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 Reentrantlock,可以显式加锁、释放锁。
 class A {
    
    
      //1.实例化ReentrantLock对象
      private final ReenTrantLock lock = new ReenTrantLook();
      public void m (){
    
    
          lock.lock//2.先加锁
          try{
    
    
              //保证线程同步的代码
          }finally{
    
    
              lock.unlock();//3.后解锁
          }
      }
  }
  //注意:如果同步代码块有异常,要将unlock()写入finally语句块中

代码示例:

class Window implements Runnable{
    
    
    private int ticket = 10;
    //1.实例化ReentrantLock,如果使用继承的方式则ReentrantLock对象必须为静态的的
    private ReentrantLock lock = new ReentrantLock();//默认构造方法的参数为false
    //private ReentrantLock lock = new ReentrantLock(true);参数为true表示公平锁,就是谁等的时间最长,谁就先获取锁
    @Override
    public void run() {
    
    
        while(true){
    
    
            try{
    
    
                //2.调用锁定方法lock()
                lock.lock();
                if(ticket > 0){
    
    
                    try {
    
    
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
                    ticket--;
                }else{
    
    
                    break;
                }
            }finally {
    
    
                //3.3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}

public class LockTest {
    
    
    public static void main(String[] args) {
    
    
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.start();
        t2.start();
        t3.start();
    }
}

1、synchronized 与 Lock 的对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
  • Lock只有代码块锁,synchronized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。

优先使用顺序:
Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)

4、处理单例模式之懒汉式的线程安全问题

1: 使用同步机制将单例模式中的懒汉式改写为线程安全的

public class BankTest {
    
    

}

class Bank{
    
    

    private Bank(){
    
    }

    private static Bank instance = null;

    public static Bank getInstance(){
    
    
        //方式一:效率稍差,当第一个线程创建了对象后,其他线程就没必要再进去了。
//        synchronized (Bank.class) {
    
    
//            if(instance == null){
    
    
//
//                instance = new Bank();
//            }
//            return instance;
//        }
        //方式二:效率更高
        if(instance == null){
    
    

            synchronized (Bank.class) {
    
    
                if(instance == null){
    
    

                    instance = new Bank();
                }

            }
        }
        return instance;
    }

}

5、死锁问题

定义

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

解决方法

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

举例

public class ThreadTest {
    
    

    public static void main(String[] args) {
    
    
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
    
    
            @Override
            public void run() {
    
    
                synchronized (s1){
    
    
                    s1.append("a");
                    s2.append("1");
                    try {
    
    
                        //如果先执行该线程,此处将其阻塞,之后有可能执行另一个线程。
                        //则s1和s2同时被上锁,就会出现死锁的状况
                        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();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (s2){
    
    
                    s1.append("c");
                    s2.append("3");
                    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();
    }
}

6、练习

 * 银行有一个账户。
 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
    分析:
 1.是否是多线程问题? 是,两个储户线程
 2.是否有共享数据? 有,账户(或账户余额)
 3.是否有线程安全问题?有
 4.需要考虑如何解决线程安全问题?同步机制:有三种方式。

class Account{
    
    
    private double balance;

    public Account(double balance) {
    
    
        this.balance = balance;
    }

    //存钱
    public synchronized void deposit(double amt){
    
    
        if(amt > 0){
    
    
            balance += amt;

            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
        }
    }
}

class Customer extends  Thread{
    
    

    private Account acct;

    public Customer(Account acct) {
    
    
        this.acct = acct;
    }

    @Override
    public void run() {
    
    

        for (int i = 0; i < 3; i++) {
    
    
            acct.deposit(1000);
        }

    }
}


public class AccountTest {
    
    

    public static void main(String[] args) {
    
    
        Account acct = new Account(0);
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

五:线程的通信

1. 三种方法

  • wait():一旦执行该方法,当前进程就进入阻塞状态,并释放同步监视器(与sleep不同的一点)。

  • notify():唤醒被wait的线程中优先级最高者。(唤醒一个)

  • notifyAll ():唤醒被wait的所有线程。(唤醒所有)

  • 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
  • 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
  • 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

在这里插入图片描述

代码实例:


使用两个线程打印 1-100,线程1, 线程2 交替打印
class Number implements Runnable{
    
    
    private int number = 1;
    Object obj = new Object();
    @Override
    public void run() {
    
    
        while(true){
    
    
            synchronized(obj){
    
    
                obj.notify();//notify()方法唤醒线程,notifyAll()唤醒所有线程
                if(number <= 10){
    
    
                    System.out.println(Thread.currentThread().getName()+"打印了:"+number);
                    number++;
                    try {
    
    
                        obj.wait();//wait()方法将线程阻塞,同时释放该线程的锁
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }else {
    
    
                    break;
                }
            }
        }
    }
}

public class ConmmunicationTest {
    
    
    public static void main(String[] args) {
    
    
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.start();
        t2.start();
    }
}

生产者消费者问题

class Clerk{
    
    
    private int num;//产品数量
    //生产产品
    public synchronized void produceProduct() {
    
    
        if(num < 20){
    
    
            num++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + num + "个产品");
            notify();//生产者
        }else{
    
    
            //当产品数量大于20时,生产者阻塞
            try {
    
    
                wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

    }
    //消费产品
    public synchronized void consumeProduct() {
    
    
        if(num>0){
    
    
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + num + "个产品");
            num--;
            notify();//消费者消费完产品后唤醒生产者
        }else{
    
    
            //没有产品时,消费者阻塞
            try {
    
    
                wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

//生产者类
class Producer extends Thread{
    
    
    private Clerk clerk;
    public Producer(Clerk clerk){
    
    
        this.clerk = clerk;
    }
    @Override
    public void run() {
    
    
        //生产产品
        while(true){
    
    
            clerk.produceProduct();
        }
    }
}

class Consumer extends Thread{
    
    
    private Clerk clerk;
    public Consumer(Clerk clerk){
    
    
        this.clerk = clerk;
    }
    @Override
    public void run() {
    
    
        //消费者消费产品
        while(true){
    
    
            clerk.consumeProduct();
        }
    }
}

public class ProductTest {
    
    
    public static void main(String[] args){
    
    
        Clerk clerk = new Clerk();
        Producer p = new Producer(clerk);
        Consumer c = new Consumer(clerk);
        p.setName("生产者");
        c.setName("消费者");
        p.start();
        c.start();
    }
}

2. wait 和 sleep 方法的异同

  • 相同点:
    (1)一旦执行方法,都可以使得当前的线程进入阻塞状态。-
  • 不同点:
    (1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()。
    (2)调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中。
    (3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,而wait()会释放同步监视器。

猜你喜欢

转载自blog.csdn.net/m0_51755061/article/details/113551324