JavaSE-多线程详解

77 阅读10分钟

线程、进程和多线程

进程是程序执行的过程,是一个动态的概念,是系统分配资源的单位。 像日常的IDE、微信都是一个进程。 线程是cpu调度和执行的基本单位,一个进程中一定会包含一个线程。 main为主线程,用于整个程序的执行。 对同一份资源操作时,会有资源抢夺的情况,需要加入并发控制。但线程的开启同时也会带来额外开销。如cpu调度时间,并发控制开销等

多线程实现

start()方法里面会抛出一个“IllegalThreadStateException”异常类对象(RuntimeException子类),每一个线程类的对象只允许启动一次,如果重复启动则就抛出此异常

public synchronized void start() {
// 当已经启动该线程时,再重复启动抛出异常
if (threadStatus != 0)
    throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
    
}

private native void start0();

继承Thread类实现多线程

  • 类继承了Thread类使这个类为线程的主体类。
  • 重写run方法
  • 调用start方法启用多线程
public class ThreadDemo extends Thread{
    // 网络图片
    private String url;
    //文件名
    private String name;

    public ThreadDemo(String url, String name){
        this.url = url;
        this.name = name;
    }
    @Override
    public void run() {
        // 重写run方法,编写线程执行体
        PicDownloader picDown = new PicDownloader();
        picDown.downloader(url, name);
        System.out.println("下载文件名为" + name);
    }

    public static void main(String[] args) {
        ThreadDemo t1 = new ThreadDemo("https://i1.hdslb.com/bfs/archive/fa5f80b1e753a9533aebacf11dc9453914761f5b.jpg@640w_400h_1c_!web-space-index-myvideo.webp", "jiaming1.png");
        ThreadDemo t2 = new ThreadDemo("https://i2.hdslb.com/bfs/archive/c5c451b83080bb14bb9900abde7f469f419ed2fc.jpg@640w_400h_1c_!web-space-index-myvideo.webp", "jiaming2.png");
        ThreadDemo t3 = new ThreadDemo("https://i2.hdslb.com/bfs/archive/a2bd9e2018050aa2afdbe059c79d7665d0908749.jpg@640w_400h_1c_!web-space-index-myvideo.webp", "jiaming3.png");

        t1.start();// 启动线程
        t2.start();
        t3.start();
    }
}

class PicDownloader {
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Runnable接口实现多线程

Runnable实现多线程,同一个对象能被多个线程实现,通过接口实现解决Java单继承的局限性(只能继承一个类)

public class ThreadDemo1 implements Runnable{
    // 票
    private int ticketCount = 10;
    @Override
    public void run() {
        while (true){
            if (ticketCount <= 0) {
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢夺第"+ticketCount--+"个签");
        }
    }

    public static void main(String[] args) {
        ThreadDemo1 t = new ThreadDemo1();
        new Thread(t, "乾坤号").start();
        new Thread(t, "神舟号").start();
        new Thread(t, "玉兔号").start();
    }
}

多个线程操作同一个资源导致并发问题和锁问题(出现-1)

image.png

callable接口实现多线程

public class ThreadCallAble {
    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread());
        new Thread(futureTask).start();
        try {
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

// 实现callable接口
class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("实现callable接口");
        return 100;
    }
}

线程状态

public class ThreadState{

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("/////");
        });

        // 观察状态
        Thread.State state = thread.getState();
        System.out.println(state);

        // 启动后
        thread.start();
        state = thread.getState();
        while (state != Thread.State.TERMINATED){
            // 线程不终止,一直输出状态
            Thread.sleep(100);
            state = thread.getState();
            // 状态
            System.out.println(state);
        }
        // 再次启动报错
        thread.start();
    }
}

image.png

线程方法

方法说明
static void sleep(long millis)让线程在指定时间内休眠
setPriority(int newPriority)更改程序的优先级
void join()等待线程停止
static void yield()暂停当前线程对象,执行其它线程
void interrupt()中断线程
boolean isAlive()检测线程是否处于活跃状态

image.png

方法说明
static void sleep(long millis)让线程在指定时间内休眠
setPriority(int newPriority)更改程序的优先级
void join()等待线程停止
static void yield()暂停当前线程对象,执行其它线程
void interrupt()中断线程
boolean isAlive()检测线程是否处于活跃状态

停止线程

  • 不推荐使用jdk提供的stop、destroy方法(已废弃)

  • 让线程自己停下来,通过设置标志位进行终止变量

    public class ThreadStop implements Runnable {
        private boolean flag = true;
        @Override
        public void run() {
            int i = 0;
            while (flag) {
                System.out.println("Thread -- run方法" + i++);
            }
        }
        public void stop() {
            this.flag = false;
        }
        public static void main(String[] args) {
            ThreadStop ts = new ThreadStop();
            new Thread(ts).start();
            for (int i = 0; i < 1000; i++) {
                System.out.println("main主线程"+i);
                if (i == 900) {
                    ts.stop();
                    System.out.println("子线程停止");
                }
            }
        }
    }
    

线程休眠

  • sleep指定线程休眠多少时间后进入就绪状态,使用时需要捕获interruptedException异常
  • sleep能够模拟网络延时、倒计时等
  • 每个对象都有一个锁,sleep不会释放锁
public class ThreadSleep2 {

    public static void tendown() throws InterruptedException {
        int num = 10;
        while (true) {
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0){
                break;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 模拟倒计时
        tendown();
        // 打印系统当前时间
        Date date = new Date(System.currentTimeMillis());
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
            date = new Date((System.currentTimeMillis()));
        }
    }
}

线程礼让

  • 当前正在执行的线程暂停,从运行转到就绪状态,但不阻塞它的运行
  • CPU重新调度还是可能会执行当前线程,也就是礼让不成功
public class ThreadYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止");
    }

    public static void main(String[] args) {
        ThreadYield yield = new ThreadYield();
        new Thread(yield, "A线程").start();
        new Thread(yield, "B线程").start();
    }
}

Join

  • 优先执行该线程,再执行其他线程,其他线程处于阻塞状态,插队机制
public class ThreadJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程VIP来了" + i);
        }
    }

    public static void main(String[] args) {
        ThreadJoin join= new ThreadJoin();
        Thread thread = new Thread(join);
        thread.start();
        for (int i = 0; i < 1000; i++) {
            if(i == 200){
                try {
                    thread.join();// main线程阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main"+i);
        }
    }
}

线程优先级

① static Thread currentThread() : 返回对当前正在执行的线程对象的引用 ==》 获取当前线程对象 ② void setName(String name) : 给线程设置名称 ③ String getName() : 获取线程的名称 int getPriority() : 返回线程的优先级
void setPriority(int newPriority) : 设置线程的优先级

线程的优先级 : 理论上优先级高的线程获取CPU分配时间片的几率更大,但是受操作系统以及jvm版本的影响,优先级的范围:1~10 10为最大,1为最小。

public class ThreadPriority{
    public static void main(String[] args) {
        TPriority priority1 = new TPriority();
        TPriority priority2 = new TPriority();

        Thread t1 = new Thread(priority1, "线程1");
        Thread t2 = new Thread(priority2, "线程2");
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(9);
        t1.start();
        t2.start();
    }
}
/**
 * 测试思路:创建两个线程对象,每个线程对象都输出100次任意内容。
 * 把第一个线程的优先级设置为10。
 * 把第二个线程的优先级设置为9.
 * 理论上第二个线程应该先输出结束
 */
class TPriority implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
    }
}

image.png

守护进程

线程:守护进程和用户进程

虚拟机只需确保用户进程执行完毕,用于后台记录操作日志,监控内存,垃圾回收等

用户线程、守护线程 : 用户线程 都结束之后 ==》 守护线程会自动结束

创建一个线程对象,默认与创建环境一致 (ex:主线程是用户线程,那么在主线程创建的线程对象默认是用户线程)

void setDaemon(boolean on) : 将该线程标记为守护线程或用户线程。 注意:①setDaemon不能在start之后调用!! ②主线程的状态不能改 ③启动之后,线程的状态不能改 ④线程的状态与创建该线程的环境状态是一致的

public class ThreadDaemon {
    public static void main(String[] args) {
        Fairy fairy = new Fairy();
        Thread f = new Thread(fairy);
        f.setName("nangong");

        Self self = new Self();
        Thread s = new Thread(self);
        s.setName("自身");

        f.start();
        s.setDaemon(true);
        s.start();
    }
}

/**
 * 测试思路:设计两个线程类,一个充当用户线程,输出10次
 * 					             一个充当守护线程,输出无限次
 * 期望效果:当用户线程输出结束,后台会自动结束
 */
class Fairy implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("日常活动:" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("线程Fairy结束");
    }
}
class Self implements  Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("守护---");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

image.png

线程同步

多个线程共用一块存储空间,会造成冲突,为了保证数据的正确性,加入了锁机制synchronized

队列加锁

当一个线程获取到对象的排它锁时,其他线程必须等待,使用后释放锁,这样会导致其它问题的产生

  • 一个线程持有锁会导致其它需要此锁的线程等待
  • 多线程下,加锁和释放锁会导致较多的上下文切换和调度延时,引起性能问题
  • 一个高优先级的线程等待优先级低的线程释放锁会导致优先级倒置,引起性能问题

线程安全问题的3种处理方式(通过线程同步)

每个synchronized方法都必须获得调用该方法对象的锁才能执行,否则会处于阻塞状态,方法独占🔒,必须等锁🔒被释放才能让其他线程获取 1.同步方法:synchronized 修饰的方法 public synchronized void test(){} 弊端:方法中的所有代码,都只允许一个线程访问。 (有一种情况:一个方法中,有一个部分代码不会涉及到线程安全问题,可以允许多个线程同时访问 == 》即下面的2.同步代码块) 2.同步代码块 : synchronized(被加锁的对象){ 代码 } 3.锁机制Lock ①创建ReentrantLock对象 ②调用lock方法:加锁 {代码....} ③调用unlock方法:解锁 注意:可把解锁的unlock方法的调用放在finally{}代码块中,保证一定能解锁

提醒:在同步的时候,其他代码都可以多个线程同时执行!只是被同步的代码不能同时执行!

Lock(锁)

◆ 从DK5.0开始, Java提供了更强大的线程同步机制一通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当. ◆ java.util.concurrent..locks.Lock接口是控制多个线程对共享资源进行访问的工具。 ◆锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象 ◆ReentrantLock可重入锁实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

public class SyncLock {
    public static void main(String[] args) {
        TestLock lock = new TestLock();
        new Thread(lock).start();
        new Thread(lock).start();
        new Thread(lock).start();
    }
}
class TestLock implements Runnable{
    int tickets = 10;

    // lock锁
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();// 加锁
                if(tickets>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("车票还剩: "+ (--tickets) + "张 !");
                }else{
                    break;
                }
            }finally {
                lock.unlock();// 解锁
            }
        }
    }
}

synchronized与Lock的对比

◆Lock是显式锁(手动开启和关闭锁,别忘记关闭锁,synchronized,是隐式锁,出了作用域自动释放 ◆Lock只有代码块锁,synchronized有代码块锁和方法锁 ◆使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类) ◆优先使用顺序:Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方 法体之外)

死锁

多个线程互相拥有对方需要的资源,然后造成死锁现象

◆产生死锁的四个必要条件: 1.互斥条件:一个资源每次只能被一个进程使用。 2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 3.不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。 4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

破环死锁产生条件避免死锁产生

线程通信

生产者消费者模式.

生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件.

◆对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又 需要马上通知消费者消费 ◆对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品 以供消费. ◆在生产者消费者问题中,仅有synchronized是不够的 ◆synchronized可阻止并发更新同一个共享资源,实现了同步 ◆synchronized不能用来实现不同线程之间的消息传递(通信)

解决线程通信问题

Java提供了几个方法解决线程之间的通信问题

方法名作用
wait()线程一直等待,直到其他线程通知,与sleep不同会释放锁
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级
别高的线程优先调度

注意:上面都是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常lllegalMonitorStateException

解决方式1

◆管程法 -> 并发协作模型“生产者/消费者模式”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

◆生产者:负责生产数据的模块(可能是方法,对象,线程,进程); ◆消费者:负责处理数据的模块(可能是方法,对象,线程,进程); ◆缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区

Producer -->数据缓存区-->Consumer

解决方式2

◆并发协作模型“生产者/消费者模式”-->信号灯法

public class PCFlag {
    public static void main(String[] args) {
        Player player = new Player();
        new Dancer(player).start();
        new Watcher(player).start();
    }
}
class Dancer extends  Thread{
    Player player;
    public Dancer(Player player){
        this.player = player;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%2==0){
                this.player.play("热剧播放中");
            }else{
                this.player.play("舞蹈节目播放");
            }
        }
    }
}

class Watcher extends Thread{
    Player player;
    public Watcher(Player player){
        this.player = player;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            player.watch();
        }
    }
}

class Player{
    String voice; // 节目
    boolean flag = true;

    public synchronized void play(String voice){
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("节目:" + voice);
        this.notifyAll();
        this.voice = voice;
        this.flag = !this.flag;
    }

    public synchronized void watch(){
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看节目"+ voice);
        this.notifyAll();
        this.flag = !this.flag;
    }
}

线程池

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。 可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具

◆好处: ◆提高响应速度(减少了创建新线程的时间) ◆降低资源消耗(重复利用线程池中线程,不需要每次都创建) ◆便于线程管理

◆corePoolSize:核心池的大小 ◆maximumPoolSize:最大线程数 ◆keepAliveTime:线程没有任务时最多保持多长时间后会终止

◆JDK5.0起提供了线程池相关API:ExecutorService和Executors ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执 行Runnable

  • Futuresubmit(Callable task):执行任务,有返回值,一般用来执行Callable

  • void shutdown():关闭连接池

◆Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

public class ThreadPool {
    public static void main(String[] args) {
        // 创建服务,创建线程池
        //newFixedThreadPool 参数为线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.execute(new CustomThread());
        service.execute(new CustomThread());
        service.execute(new CustomThread());
        service.execute(new CustomThread());

        //关闭连接
        service.shutdown();

    }
}

class CustomThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}