Java的三魂七魄 —— 高级多线程

Java的三魂七魄 —— 高级多线程

目录

  • Java的三魂七魄 —— 高级多线程
    • 一、多线程的创建
    • 二、线程安全问题
    • 三、线程通信问题
    • 四、更多实例
      • 1.用线程同步的方法解决单例模式的线程安全问题
      • 2.银行存钱问题(线程安全问题)
      • 3.生产者消费者问题(线程通信问题)

一、多线程的创建

多线程的创建方法有:四种!!! #F44336

1.创建Thread子类

先上代码:

/**
 * 创建多线程的方法一:
 *      创建继承Thread的子类
 * 
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/7 18:41
 */

//线程类
class NumCount extends Thread{
     

    //run方法里是要执行的代码
    @Override
    public void run() {
     
        //输出0-99
        for (int i = 0; i < 100; i++) {
     
           System.out.println(NumCount.currentThread().getName()+":"+i);
        }
    }
}
//主类
public class MyThread {
     
    public static void main(String[] args) {
     
        //创建Thread子类的对象
        NumCount nc1 = new NumCount();
        //给线程起个名字
        nc1.setName("计数线程1");
        //开启线程
        nc1.start();
    }
}

注意事项

  • run()方法为线程的执行内容
  • 创建的对象需要调用start()方法开启线程(继承)
  • NumberCount.currentThread().getName()是获取当前线程的名称 (继承)

2.实现Runnable接口

先上代码:

/**
 * 创建多线程的方法二:
 *      实现Runnable接口
 *
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/7 18:41
 */

//线程类
class NumCount implements Runnable{
     

    //run方法里是要执行的代码
    @Override
    public void run() {
     
        //输出0-99
        for (int i = 0; i < 100; i++) {
     
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
//主类
public class MyThread {
     
    public static void main(String[] args) {
     
        //实例化Runnable接口
        NumCount numberCount = new NumCount();
        //实例化一个线程,构造方法的参数为实例化的接口
        Thread nc1 = new Thread(numberCount);
        //给线程起个名字
        nc1.setName("计数线程1");
        //开启线程
        nc1.start();
    }
}

注意事项

  • 由于是实现Runnable接口,获取线程名称时不能用NunberCount类
  • 创建线程时需要先实例化接口,然后创建线程对象

3.实现Callable接口

先上代码:

/**
 * 创建多线程的方法三:
 *      实现Callable接口  --- JDK5.0新增
 *
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/7 18:41
 */

//线程类
class NumCount implements Callable<Integer> {
     

    //重写call()方法
    @Override
    public Integer call() throws Exception {
     
        //计算1-100的数的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
     
            System.out.println(Thread.currentThread().getName() + ":" + i);
            sum += i;
        }
        return sum;
    }
}
//主类
public class MyThread {
     
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
        //创建Callable接口实现类的对象
        NumCount numCount = new NumCount();
        //将Callable接口实现类的对象传递到FutureTask构造器,创建FutureTask对象
        FutureTask<Integer> futureTask = new FutureTask<>(numCount);
        //创建线程,传递FutureTask对象到Thread构造器中
        Thread nc1 = new Thread(futureTask);
        nc1.setName("计数线程1");
        nc1.start();

        //获取Callable中call方法的返回值
        Integer sum = futureTask.get();
        System.out.println("总和为:" + sum);
    }
}

注意事项

  • Callable方法更新于jdk 1.5
  • FutureTask实现了Runnab了接口,所以创建线程时可以传入FutureTask的实例化对象
  • Callable相较于Runnable更为灵活,支持泛型,可以回传值
  • call()方法的返回值由FutureTask的get()方法获取

4.创建线程池

先上代码:

/**
 * 创建多线程的方法四:
 *      创建线程池
 *
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/7 18:41
 */

//线程类
class NumCount implements Runnable {
     

    //重写run方法
    @Override
    public void run() {
     
        for (int i = 0; i < 100; i++) {
     
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
//主类
public class MyThread {
     
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
        //创建指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumCount());//用于Runnable
//        service.submit();//用于Callable
        //关闭线程池
        service.shutdown();
    }
}

注意事项

  • 线程池支持Runnable和Callable对象
  • 详细信息看另一篇线程池详解

二、线程安全问题

问题引入:

  • 火车票问题: 三个售票窗口同时卖100张票

问题分析:

  1. 三个窗口同时买票需要使用多线程
  2. 买票过程中会有一个共享数据:票数
  3. 执行过程中会出现线程安全问题:重复卖同一张票和卖出错票

Q: 为什么会重票错票?
A: 当出现极限情况:当多个线程同时调用共享数据,并且线程还未结束,就会出现重复调取同一个值的情况(重票),票数不满足线程数时,会出现负数(错票)。

解决方案:线程同步机制

线程同步机制:
当某一个线程在使用共用的数据(执行被同步的代码)时,其他线程要进行等候,无论这个线程是否处于阻塞状态。

	如果不明白的话,就想象一下旅游景点女厕所门口排队的女性朋友们。

具体办法:

1. 同步代码块
这个方法涉及到synchronized修饰词。
基本格式:

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

说明:

  1. 操作共享数据的代码即为要被同步的代码
  2. 共享数据:多个线程要共同操作的同一个数据
  3. 同步监视器,俗称锁,任意一个类的对象都可担任。
  4. 同步的多个线程必须共用一把锁

使用示例:

/**
 * 用同步代码块解决线程安全问题
 *
 * 问题:三个窗口卖100张票,用线程解决。
 *
 *
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/5 17:50
 */

class TicketTread extends Thread{
     

    //保证多个线程共用一份数据,需要设置成静态的
    public static int ticket = 100;
    //同一把锁
    public static Object object = new Object();

    @Override
    public void run() {
     
        while (true){
     
            //同步代码块
            synchronized (object){
     
                if (ticket>0){
     
                    try {
     
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                    System.out.println(TicketTread.currentThread().getName() + ":正在售卖第" + ticket + "张票");
                    ticket--;
                }else{
     
                    break;
                }
            }
        }
    }
}


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

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

2. 同步方法
这个方法涉及到synchronized修饰词。
基本格式:

synchronized 数据类型 方法名(){
     
	//需要被同步的代码
}

注意:

  • 这个时候还是有同步监视器(锁),默认为this

使用示例:

class Windows1 implements Runnable{
     

    //这里无需设定静态变量,因为多个线程调用同一个接口
    public int ticket = 100;
    Object object = new Object();

    @Override
    public void run() {
     
        while (true){
     
               show();
        }

    }
	//同步方法
    public synchronized void show(){
     
        if (ticket>0){
     
            try {
     
                Thread.sleep(100);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }

            System.out.println(TicketTread.currentThread().getName() + ":正在售卖第" + ticket + "张票");
            ticket--;
        }
    }
}


public class TicketDemo2 {
     
    public static void main(String[] args) {
     
        Windows1 windows = new Windows1();
        Thread t1 = new Thread(windows);
        Thread t2 = new Thread(windows);
        Thread t3 = new Thread(windows);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

3. ReentrantLock
jdk1.5新特性
用法和同步代码块类似。用调用方法的办法替代代码块。
使用示例:

class Windows implements Runnable{
     

    public static int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
     
        while (true){
     
            try {
     

                //调用锁定的方法
                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 {
     
                //调用解锁的方法
                lock.unlock();
            }

        }
    }
}

public class LockTest {
     
    public static void main(String[] args) {
     
        Windows windows = new Windows();
        Thread t1 = new Thread(windows);
        Thread t2 = new Thread(windows);
        Thread t3 = new Thread(windows);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

面试题:synchronized 和 Lock 的区别?

  • 相同点:都是实现线程同步机制
  • 不同点:
    • synchornized机制在执行完相应的同步代码以后,自动释放同步监视器;
    • lock需要手动的启动同步(lock()),同时结束同步也需要手动实现(unlock())

三、线程通信问题

问题引入:

两个线程打印1-100,交替打印
问题代码:

/**
 * 两个线程打印1-100,交替打印
 *
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/6 22:41
 */

class Number implements Runnable{
     

    private int number = 1;

    @Override
    public void run() {
     
        while (true){
     
            synchronized (this) {
     
                if (number<=100){
     
                    try {
     
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":数字:" + number);
                    number++;
                }else {
     
                    break;
                }
            }
        }
    }
}

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

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

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

当你运行此段代码时,会出现仅单一线程运行的情况

问题分析:

t1线程一致占用锁,无法进行交替打印

解决方案:线程通信

需要利用三剑客:wait(),notify(),notifyAll()

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器(锁)
  • notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

说明:

  1. 三个方法必须使用在同步代码块或同步方法中
  2. 三个方法的调用者,必须是同步代码块或同步方法中的同步监视器(锁为调用者,只有锁才能调用三个方法)否则,会出现IllegalMonitorStateException异常
  3. 三个方法定义在java.lang.Object类中

具体办法:

/**
 * 两个线程打印1-100,交替打印
 *
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/6 22:41
 */

class Number implements Runnable{
     

    private int number = 1;

    @Override
    public void run() {
     
        while (true){
     
            synchronized (this) {
     
                //使用notify()方法唤醒线程
//              this.notify();
                notify();
                if (number<=100){
     
                    try {
     
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":数字:" + number);
                    number++;

                    try {
     
                        //使用wait()方法使得线程处于阻塞状态
//                      this.wait();
                        wait();
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }

                }else {
     
                    break;
                }
            }
        }
    }
}

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

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

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

面试题:sleep() 和 wait() 的异同?

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

四、更多实例

1.用线程同步的方法解决单例模式的线程安全问题

问题解决:

/**
 * 用线程同步的方法解决单例模式的线程安全问题
 *
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/6 16:32
 */
public class BankDemo {
     
}

class Bank{
     
    //单例模式(private构造方法,确保该类使用对象唯一)
    private Bank(){
     };

    public static Bank instence = null;

    //此处可能引发线程安全问题
    public static Bank getInstence(){
     

        //方式一,效率稍差(线程全部同步)
//        synchronized (Bank.class) {
     
//            if (instence==null){
     
//                instence = new Bank();
//            }
//        return instence;
//        }
        //方式二:效率较高(一小部分线程同步)
        if (instence==null){
     
            synchronized (Bank.class) {
     
                if (instence==null){
     
                    instence = new Bank();
                }
            }
        }
        return instence;
    }
}

2.银行存钱问题(线程安全问题)

问题描述:
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000,存3次,每次存完打印账余额。
问题解决:

/**
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/6 22:19
 */

//账户(共享数据)
class Account{
     

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

    //存钱
    public synchronized void deposit(double amt){
     

        if (amt > 0){
     
            balance += amt;
            System.out.println(Thread.currentThread().getName() + ":存钱成功!当前余额为:" + balance);
        }
    }
}

//储户(线程)
class Customer implements Runnable{
     

    private Account acct;
    //初始化数据
    public Customer(Account acct){
     
        this.acct = acct;
    }

    @Override
    public void run() {
     
        for (int i = 0; i < 3; i++) {
     
            try {
     
                Thread.sleep(1000);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            acct.deposit(1000);
        }
    }
}

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

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

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

3.生产者消费者问题(线程通信问题)

问题描述:
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品。店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
问题分析:

1.是否是多线程问题?是,生产者线程,消费者线程
2.是否有共享数据?是,店员(或产品)
3.如何解决现成的安全问题?同步机制,有三种方法
4.是否设计线程的通信?是

问题解决:

/**
 * @author ☂࿈秋鹜࿈️
 * @create 2020/3/7 11:04
 */
class Clerk{
     

    private int productCount = 0;

    //生产产品
    public synchronized void produceProduct() {
     
        if (productCount<20){
     
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":正在生产第" + productCount + "个产品");
            notify();
        }else {
     
            try {
     
                wait();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
    }
    //消费产品
    public synchronized void consumeProduct(){
     
        if (productCount>0){
     
            System.out.println(Thread.currentThread().getName() + ":正在消费第" + productCount + "个产品");
            productCount--;
            notify();
        }else {
     
            try {
     
                wait();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
    }
}

class Producer implements Runnable{
     //生产者

   private Clerk clerk;

    public Producer(Clerk clerk){
     
        this.clerk = clerk;
    }

    @Override
    public void run() {
     
        System.out.println(Thread.currentThread().getName() + ":开始生产产品....");

        while (true){
     
            try {
     
                Thread.sleep(10);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}

class Consumer implements Runnable{
     //消费者

    private Clerk clerk;

    public Consumer(Clerk clerk){
     
        this.clerk = clerk;
    }

    @Override
    public void run() {
     
        System.out.println(Thread.currentThread().getName() + ":开始消费产品....");

        while (true){
     
            try {
     
                Thread.sleep(20);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}

public class Product {
     
    public static void main(String[] args) {
     
        Clerk clerk = new Clerk();

        Producer producer = new Producer(clerk);
        Thread p1 = new Thread(producer);
        p1.setName("生产者1");

        Consumer consumer = new Consumer(clerk);
        Thread c1 = new Thread(consumer);
        c1.setName("消费者1");
        Thread c2 = new Thread(consumer);
        c2.setName("消费者2");

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

你可能感兴趣的:(java技巧,java,多线程,编程语言)