JavaSE基础-多线程

169 阅读8分钟

一、多线程

1. 多线程实现的两种方式

1).extends Thread

//1. 继承Thread类
public class MyThread extends Thread{
    //2. 重写run()方法
    @Override
    public void run(){
        //code
    }
}

public static void main(String[] args){
    //3. 创建MyThread对象
    MyThread myThread = new MyThread();
    //4. 调用start()方法
    myThread.start();
}

2).implements Runnable

//1. 实现Runnable接口
public class MyRunnable implements Runnable{
    //2. 重写run()方法
    @Override
    public void run(){
        //code
    }
}
public static void main(String[] args){
    //3. 创建MyRunnable对象
    MyRunnable myRunnable = new MyRunnable();
    
    //4. 创建Thread对象
    
    /*
    Thread类中存在两个构造方法:
    a. Thread(Runnable runnable);
    b. Thread(Runnbale runnable,String threadName);
    */
    
    Thread thread = new Thread(myRunnable);
    
    //5. 调用start()方法
    thread.start();
}

2. Thread和Runnable的区别

a. implements Runnable避免了单继承的局限性 b. 增强了程序的扩展性,降低了程序的耦合性(解耦):

设置线程任务开启新线程进行了分离(解耦)

二、线程安全

1.线程安全问题描述

多线程程序访问共享数据时,会出现线程安全问题

2.线程安全问题产生图解

3.线程的六种状态

Thread.State

三、线程安全解决办法

1.同步代码块

1).代码格式

synchronized(同步锁){
    需要同步操作的代码,即可能出现线程安全问题的代码
}

2).注意事项

a. 同步代码块中的锁对象,可以使用任意的对象

b. 必须保证多个线程使用的锁对象是同一个

c.锁对象的作用:

同步代码块锁住,只让一个线程在同步代码块中执行

3).同步代码块的技术原理

使用了一个锁对象,也叫对象锁,也叫对象监视器

第一个线程抢到了cpu执行权,执行run()方法,遇到synchronized(锁对象){}代码块,检查其中是否有锁对象,如果有,就进入同步代码块执行

此时,如果第二个线程抢到了cpu执行权,执行run()方法,遇到synchronized(锁对象){}代码块,检查其中是否有锁对象,发现没有(第一个线程没执行完成,还未归还锁对象),则进入阻塞状态,直到获取到第一个线程执行完成归还的锁对象,才能进入到同步代码块中执行

总结:

a. 同步代码块中的线程,没有执行完毕不会释放锁
b. 同步代码块外的线程,没有锁不能进入执行

4). 同步代码块存在的问题

程序频繁地判断锁获取锁释放锁降低效率

2.同步方法

1). 代码格式

public synchronized void payTicket(){
    //可能出现线程安全问题的代码(访问了共享数据的代码)
}

2). 同步方法技术原理

定义一个同步方法,也会把方法内部的代码锁住,只让一个线程执行,同步方法的锁对象就是实现类对象,就是this

3). 与同步代码块的辨析

a. 同步代码块显式使用锁对象进行线程同步

b. 同步方法隐式使用实现接口的类对象,即this作为锁对象

4). 静态同步方法

a. 格式

public class MyRunnable implements Runnable{
    //静态方法只能访问静态变量
    static int ticket = 100;
    
    @Override
    public void run(){
        while(true){
            if(ticket > 0){
                sellTicketStatic();
            }else{
                break;
            }
        }
    }
    
    public static synchronized void sellTicketStatic(){
        if(ticket > 0){
            System.out.println("正在售卖第"+(ticket--)+"张票");
        }
    }
}

b. 注意

a). new Thread(Runnable实现类)对象即使调用了start()方法,也并不是一口气把run()方法跑完,因此执行一段时间sellTicketStatic()方法后,会出现新的线程抢占执行权的情况,继续再执行run()方法调用的sellTicketStatic()方法,因此sellTicketStatic()中不能写while()循环,否则就变成一个线程抢占到执行权,一直执行完毕(单线程)

b).静态同步方法的锁对象不是this,因为静态方法早于对象创建而加载的,因此,其锁对象本类的class属性,即class文件对象(反射)

synchronized(MyRunnable.class){ //同步代码块 }

3.锁机制

JDK1.5之后,新增java.util.concurrent.locks.Lock接口

1).意义

Lock实现了比使用同步的方法更广泛锁定操作。此实现允许更灵活的结构,可以具有差别很大属性,可支持多个相关的Condition对象

2).常用方法

a. void lock()获取锁 b. void unlock()释放锁

3).Lock接口实现类

java.util.concurrent.locks.Reentrantlock implements Lock

4).使用步骤

a. 在类中成员位置处创建Reentrantlock类对象

b. 在可能出现线程安全问题代码前调用Lock接口的lock()方法获取锁

c. 在可能出现线程安全问题代码后调用Lock接口的unlock()方法释放锁

5).代码解析

a. 错误代码

public class MyRunnable implements Runnable{

    int ticket =100;
    Lock lock = new ReentrantLock();
    
    @Override
    public void run(){
        while(true){
            lock.lock();
            if(ticket > 0){
                try{
                   Thread.sleep(100);
                   System.out.println("正在售卖第"+(ticket--)+"张票");
                }catch(Exception e){
                    e.printStackTrace();
                }finally{
                    //无论是否出现异常,都会将锁释放掉
                    lock.unlock();
                }
            }else{
                System.out.println("当前进程未结束"+ Thread.currentThread().getName());
                break;
            }
        }       
    }
}

b. 代码更正

public class MyRunnable implements Runnable{

    int ticket =100;
    Lock lock = new ReentrantLock();
    
    @Override
    public void run(){
        while(true){
            lock.lock();
            if(ticket > 0){
                try{
                   Thread.sleep(100);
                   System.out.println("正在售卖第"+(ticket--)+"张票");
                }catch(Exception e){
                    e.printStackTrace();
                }finally{
                    //无论是否出现异常,都会将锁释放掉
                    lock.unlock();
                }
            }else{
                lock.unlock();
                System.out.println("当前进程已结束"+ Thread.currentThread().getName());
                break;
            }
        }       
    }
}

c. 原因解析

a中的代码错误在于:

当一个新的线程抢占到执行权时,如果刚好此时ticket <= 0,进入else语句,线程未解锁,执行了打印语句,但无法执行break语句,具体原因未知

b中更正代码解释:

线程执行run()方法时,一个新的线程抢占到执行权后,当前线程会调用finally中的代码释放锁,以让新的线程继续执行,考虑另一种情况,如果此时刚好ticket <= 0,则新线程释放锁跳出循环,问题在于创建的三个线程都会执行else{}中的代码,因为他们共同写入了MyRunnable参数,主函数中没有代码告诉他们已经可以跳出循环了,三个线程分别跑到run()方法中,发现可以跳出循环,故需要线程间通信

四、等待唤醒机制(线程间通信)

1. 定义

多个线程处理同一个资源,但各线程的线程任务不同,需要有规律执行各线程

Tips: 可理解为合作关系,比如一个线程生产包子,另一个线程吃包子,如果多个线程处理同一个资源,且任务相同,类似于竞争关系,上述买票即如此,需要三个窗口线程分别确认是否跳出循环

2. 概述

任务不同的各线程通过判断共享资源的状态执行等待执行操作

3. 常用方法

1). wait()

线程不再抢夺CPU执行权,进入线程所属对象的wait set中,不浪费CPU资源不竞争锁,线程状态是WAITING。当前线程需要等待别的线程执行通知notify()方法在当前线程所属对象上,使其从wait set中释放出来,重新进入到调度队列(ready queue)中

2). notify()

选取所通知对象wait set中的一个线程释放,一般选取其中等待时间最长的线程

3). notifyAll()

释放所通知对象wait set中的全部线程

4). wait(long)

线程进入TimeWaiting状态,参数long时间后,自动唤醒

5). sleep(long)

线程进入TimeWaiting状态,参数long时间后,自动开启

4. 注意

通知一个等待的线程,该线程也不会立即恢复执行,因为中断的位置在同步块内,而此时他并未持有锁,需要再次尝试获取锁(依旧面临其他线程的竞争),竞争到锁后才能在当初调用wait()方法之后的地方恢复执行

Tips:

a. 如果线程获取到锁,则从WAITING状态变成RUNNABLE状态
b. 否则,从wait set出来,又进入entry set,线程从WAITING(等待)状态变成BLOCKED(阻塞)状态

5. 方法调用细节

1). wait()方法与notify()方法必须由同一个锁对象调用。因为对应的锁对象可以通过notify()唤醒使用该锁对象调用wait()方法以等待的线程

2). wait()方法与notify()方法同属于Object

3). wait()方法与notify()方法必须要在同步代码块或者同步方法中使用,因为需要使用锁对象调用这两个方法

五、线程池

1. 意义

如果线程数量很多,而任务简单,频繁创建线程实则降低效率

需要线程复用,执行完线程并不被销毁,而是转而执行其他任务

2. 概念

容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,避免消耗过多资源

3. 本质

LinkedList

JDK1.5之后,内置了线程池函数

4. 详述

  1. java.util.concurrent.Executors:线程池工厂类,用来生成线程池

  2. Executors类中静态方法:static ExecutorService newFixedThreadPool(int nTheads):创建一个可复用固定线程数的线程池

  3. params: nThreads创建线程池中包含的线程数量

  4. returns: ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)

  5. java.util.concurrent.ExecutorService:线程池接口,其中有一个方法用来从线程池中获取线程,调用start方法,执行线程任务:

    submit(Runnable task)提交一个Runnable任务用于执行

  6. 关闭/销毁线程池的方法

    void shutdown()

5. 使用

  1. 使用线程池的工厂类Executors里提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
  2. 创建一个类,实现Runnbale接口,重写run()方法,设置线程任务
  3. 调用ExecutorService中的submit( Runnable task)方法,传递线程任务,开启线程,执行run()方法
  4. 可以但不建议调用executorService中的shutdown()方法关闭/销毁线程池