【SE】多线程-02

文章目录

1、守护线程

【SE】多线程-02

2、线程的生命周期

【SE】多线程-02
【SE】多线程-02

3、线程的同步

1、问题描述

当多条语句操作同一个线程共享数据时,一个线程中的多条语句只执行了一部分,并没有执行完全,但此时另一个线程参与进来,导致共享数据的错误
也是因为这种原因,导致多线程具有了安全问题
【SE】多线程-02
【SE】多线程-02

2、解决办法

  • 同步代码块/同步方法
  • Lock接口

4、同步代码块与同步方法

【SE】多线程-02
对上图的一些解释:
操作共享数据的代码即需要被同步的代码
多个线程共同操作的对象即共享数据
同步监视器也称为锁,可以是任意类型的对象(类也是对象),但必须全局唯一
【SE】多线程-02

1、同步代码块相关

共享变量的选取:

  • 任何对象都可以做共享数据,但通常情况下不会new一个对象来专门做共享变量
  • 在实现Runnable接口时,由该Runnable接口创建的多个Thread均通过同一个Runnable实现类构造,因此实现类中定义的变量天然唯一,也就是可以通过this关键字来获得共享数据
  • 当线程类是由继承Thread得到时,就不能使用this关键字了,因为多个线程创建的多个对象并非唯一对象,因此需要使用类对象做共享变量,即定义的Thread子类.class

2、同步方法相关

  • 操作共享数据的代码完整地声明在一个方法中,可将该方法声明为同步方法
  • 注意:
    【SE】多线程-02
  • 上面的代码可以将除while外的代码进行抽取作为新的方法,因此引出下一个问题
  • synchronized声明在同步方法时,锁住的是this对象
    • 如果通过Runnable接口创建的类,通过run方法或run方法内部调用其他同步方法时,是不会出现问题的,因为在上面提到过:Runnable实现类中定义的变量天然唯一,也就是可以通过this关键字来获得共享数据
      【SE】多线程-02

    • 但如果通过继承Thread类创建的类,是有多个对象的,因此需要给同步方法添加static,保证唯一性,也就是类锁
      【SE】多线程-02
      如果不加static,可以看到也是线程不安全的【SE】多线程-02

【SE】多线程-02
【SE】多线程-02

5、单例模式【懒汉】优化

  • 线程不安全的单例模式
public class Singleton {
}

//下面的类是一个懒汉式的单例模式,当且仅当该类getInstance()被调用时才实例化出唯一的类对象
class Bank {
    private Bank() {
    }

    private Bank instance = null;

    public Bank getInstance(){
        if ( instance == null){
            instance = new Bank();
        }
        return instance;
    }
}
  • 如果在实例化前有多个线程同时调用了getInstance(),就会出现线程不安全的情况
    解决方法:加锁
  • 方式一
//方式一:将获取类实例的方法设置为同步方法,将该类锁住,直到某线程调用完成方法,实例化出新的对象后才释放该锁
//这种方法同样会造成获取类实例时效率不高的问题,因为当类没有实例化前,多个线程抢占实例化的,进入方法后加锁的操作是合理的,可当对象实例化之后,对对象的获取也需要加锁显然会拖慢获取速度,因此需要进行优化
public static synchronized Bank getInstance(){
    if ( instance == null){
        instance = new Bank();
    }
    return instance;
}
  • 方式二
// 方式二:同步代码块,效率较低,和方式一意思一样
// 最开始几个线程抢占初始化Bank实例对象,可实例化出对象后还需要加锁获取已经实例化出的对象,明显不合理
public static Bank getInstance(){
    synchronized (Bank.class) {
        if (instance == null) {
            instance = new Bank();
        }
        return instance;
    }
}
  • 方式三
//方式三:设置标志位,通过标志位判断是否加锁,避免后期加锁获取已有实例对象,效率较高
public static Bank getInstance() {
    if (instance == null) {
        synchronized (Bank.class) {
            if (instance == null) {
                instance = new Bank();
            }
        }
    }
    return instance;
}

6、死锁
【SE】多线程-02
死锁举例
线程1进入run方法后,对s1对象进行加锁,进行相应操作后,为了使死锁现象出现,主动使其调用sleep(),给第二个线程充足的时间锁住s2,当另一个线程也锁住s2且该线程sleep()调用完毕后,尝试锁住s2,但s2此时已经被锁住,同样另一个线程也尝试锁s1,两个线程都在等待对方线程释放自己需要的资源,同时又不释放自己拥有的资源,因此造成死锁情况

public class DeadLock {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    System.out.println("成功锁住s1");
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(200);
                    } 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){
                    System.out.println("成功锁住s2");
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                    }
                    System.out.println(s1);
                    System.out.println(s2);
                }
            }
        }).start();

    }
}

运行结果:
【SE】多线程-02

6、Lock锁

  • Lock介绍
    【SE】多线程-02
    【SE】多线程-02
  • 代码演示
    【SE】多线程-02
  • Lock锁和synchronized关键字异同:
    【SE】多线程-02
  • Lock使用案例
    【SE】多线程-02
    代码实现
class Account{
    private double balance;

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

    public void deposit(double money){
        if (money>=0) {
            balance += money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"存钱,当前余额为:"+balance);
        }

    }
}

class AccountThread extends Thread{
    private static ReentrantLock lock = new ReentrantLock();
    private Account account;

    public AccountThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        try {
            lock.lock();
            for (int i=0;i<5;i++){
//          注意:这里也可以通过给Account中的deposit()添加synchronized关键字来解决线程安全问题
//          并且可以直接使用synchronized(this)来给当前account对象加锁,而在多个线程的run中直接调用该方法
//          前面虽然说通过继承的方法在编写同步代码块的时候不能直接通过this而要通过类名.class
//          进行同步,但它指的是如果synchronized声明在run()中也就是继承了Thread类的类中才不能直接使用this
//          本例中,同步块并没有定义在run中而是其他类中,因此还是唯一的
                account.deposit(1000);
            }
        }finally {
            lock.unlock();
        }

    }
}
  • 代码测试
public class DepositMoneyTest {
    public static void main(String[] args) {
        Account account = new Account(0);
        AccountThread t1 = new AccountThread(account);
        AccountThread t2 = new AccountThread(account);

        t1.setName("1");
        t2.setName("2");

        t1.start();
        t2.start();
    }
}

运行结果
【SE】多线程-02

7、线程的通信【wait与notify、notifyAll】

  • 问题描述
    【SE】多线程-02

  • 代码实现
    【SE】多线程-02
    注意:notify默认情况下,其调用者为this
    因此在同步代码块中锁定this对象时,不通过this.notify()而直接使用notify()也是可以的
    而当同步对象为其他对象时,那么必须显式通过对象名.notify()进行调用,否则会报错,详见下面代码

public class Number implements Runnable{

    private int number = 1;
    private Object object = new Object();

    @Override
    public void run() {
        while (true){
            synchronized (object){
//      唤醒当前阻塞的线程,并且这里调用的是this.notify()
                object.notify();       
//      假设当前线程1成功进入该同步代码块并执行相应方法,执行后 调用wait方法,被动阻塞,此时线程1无法再继续运行下去
//      调用wait方法会释放锁,因此线程2成功进入该同步块并唤醒1,而此时2拥有锁,因此1仍不能抢占锁资源
                if (number<=100){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+": "+number);
                    number++;
                    try {
                        object.wait();         //执行完操作就将线程阻塞
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }

            }
        }
    }

    public static void main(String[] args) {
        Number number = new Number();
        Thread thread1 = new Thread(number);
        Thread thread2 = new Thread(number);

        thread1.setName("1->");
        thread2.setName("2->");
        thread1.start();
        thread2.start();
    }
}

【SE】多线程-02
notify()会优先唤醒处于wait状态中优先级最高的线程
【SE】多线程-02
【SE】多线程-02

8、有关是否释放锁

【SE】多线程-02
【SE】多线程-02
【SE】多线程-02
综上:wait()、join()(底层也是wait())会释放锁,而sleep()、yield()不会释放锁

理解实现线程通信的三个方法均出自Object类:

  • 调用者是同步监视器(三个方法的调用者均必须是同步代码块/同步方法中的同步监视器,且同步方法的默认同步监视器为调用它的对象)
  • 同步监视器可以是任意对象
  • 也就是说任意对象都可以调用这三种方法
  • 因此wait()/notify()/notifyAll()均属于Object类中的方法

9、生产者、消费者问题

【SE】多线程-02
【SE】多线程-02
生产者线程内部调用店员类中定义的生产方法
【SE】多线程-02
消费者线程内部调用店员类中定义的消费方法
【SE】多线程-02
生产者线程和消费者线程均通过店员类进行创建,因此共享店员类中定义的商品数
【SE】多线程-02

10、JDK5.0新增创建线程方法

创建多线程的方法共有4种:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用线程池

1、Callable接口

1、基本介绍

【SE】多线程-02
【SE】多线程-02

2、通过Callable新建线程过程
  1. 创建实现Callable接口的实现类
  2. 实现call(),将此线程中需要实现的方法声明在其中
  3. 创建实现了Callable接口的类的实例对象callable
  4. 将callable作为构造器参数传入到FutureTask类中,创建FutureTask类实例对象futureTask
    public class FutureTask<V> implements RunnableFuture<V>
    public interface RunnableFuture<V> extends Runnable, Future<V>
    可以看出FutureTask类底层也实现了Runnable接口,因此可以作为参数传递到Thread类中进行线程的创建
  5. 若需要获取call()方法中的返回值,则需要通过调用futureTask.get()对其进行获取,不需要获取则不需要调用其get()
  6. 将futureTask作为参数传入Thread构造函数中,创建线程,通过调用start()开启线程执行相应call()中操作

代码实例:

package callable;/**
 * @author zhihua.li
 * @date 2021/3/23 - 10:48
 **/

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 *@program: SE Reviewed
 *@description:
 *@author: zhihua li
 *@create: 2021-03-23 10:48
 */
/*
* 1、创建实现Callable接口的实现类
* 2、实现call(),将此线程中需要实现的方法声明在其中
* 3、创建实现了Callable接口的类的实例对象callable
* 4、将callable作为构造器参数传入到FutureTask类中,创建FutureTask类实例对象futureTask
*       public class FutureTask<V> implements RunnableFuture<V>
*       public interface RunnableFuture<V> extends Runnable, Future<V>
*       可以看出FutureTask类底层也实现了Runnable接口,因此可以作为参数传递到Thread类中进行线程的创建
* 5、若需要获取call()方法中的返回值,则需要通过调用futureTask.get()对其进行获取,不需要获取则不需要调用其get()
* 6、将futureTask作为参数传入Thread构造函数中,创建线程,通过调用start()开启线程执行相应call()中操作
*/

public class CallableTest implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i%2==0){
                System.out.println(i);
                sum+=i;
            }
        }
        return sum;
    }

    public static void main(String[] args) {
        CallableTest callable = new CallableTest();
        FutureTask futureTask = new FutureTask(callable);
        new Thread(futureTask).start();
        try {
//            get()的返回值即为futureTask构造器参数中实现了Callable接口的类对象重写的call()的返回值
            Integer sum = (Integer) futureTask.get();
            System.out.println("100内偶数总和为:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
3、Callable为什么比Runnable功能更加强大?
  • call()可以有返回值
  • call()可以抛出异常,并被外面的操作捕获,对调用者透明显示错误信息
  • Callable支持泛型

2、线程池

1、相关介绍

【SE】多线程-02
【SE】多线程-02

2、代码实现
package threadPool;/**
 * @author zhihua.li
 * @date 2021/3/23 - 11:27
 **/

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @program: SE Reviewed
 * @description:
 * @author: zhihua li
 * @create: 2021-03-23 11:27
 */
public class ThreadPoolTest {
    public static void main(String[] args) {
//        创建一个包含10个线程的线程池,但此时并没有分配线程
        ExecutorService service = Executors.newFixedThreadPool(10);
//        执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类对象
        service.execute(new NumberThread());      //打印偶数线程
        service.execute(new NumberThread1());     //打印奇数线程
//        service.submit();         //适用于Callable
        service.shutdown();         //关闭线程池
    }
}

class NumberThread implements Runnable {

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

class NumberThread1 implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i%2!=0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
3、关于线程管理

【SE】多线程-02

上一篇:Java SE 运算符


下一篇:Java SE-面向对象