zl程序教程

您现在的位置是:首页 >  后端

当前栏目

有趣的多线程和无趣的线程锁

2023-06-13 09:17:10 时间

Java 中的多线程实现较为简单,这篇文章主要讲解多线程操作中状态操作相关的代码示例,希望可以帮助你提高对多线程的理解。

要学习多线程首先要了解进程和线程还有多线程的区别是什么

进程

在开启一个软件后,操作系统会分配给软件一个进程,进程即该软件所在的内存区域,是软件运行时状态的一个抽象,是一个整体,进程中必须包含线程,不可独立存在。

线程

线程的宿主是进程,一个进程代表一个软件,线程为一个进程中正在并行执行的一些功能,打个比方,QQ 有接收消息的功能还有上线提醒的功能,它们同时进行,互不干扰。

多线程

在多核 CPU 中,通过软件或者硬件来实现多个核心协同工作,达到充分使用 CPU 的目的。在单核CPU时,操作系统会进行伪多线程处理,即每个线程随机短暂运行并快速切换,实现多任务处理。

创建线程的两种方式

继承 Thread 对象

class DemoTask extends Thread {
    public static List<String> list = new ArrayList<>();

    @Override
    public void run() {
        System.out.println("hello Thread");
    }
}

实现 Runnable 接口

class DemoTask implements Runnable {
    public static List<String> list = new ArrayList<>();

    @Override
    public void run() {
        System.out.println("hello Runnable");
    }
}

实际开发中,推荐实现 Runnable 接口,因为 Java 是支持多实现的但是仅仅支持单继承,如果继承了 Thread 对象,那就不能继承其他对象了。

线程中的相关方法

Sleep - 休眠

使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。

public class TestRunnable {
    public static void main(String[] args){
        NewRunnable runnable = new NewRunnable();
        //分别创建两个线程,并传入runnable参数
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        //运行线程
        thread1.start();
        thread2.start();
        System.out.println("End....");
    }
}
class NewRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述

yield - 放弃

该方法主要作用是当前线程主动放弃时间片,回到就绪状态,竞争下一次的时间片,比如说 A 和 B 两个进程 A 运行一段时间后 CPU 分配运行权利给了 B,但是 B如果调用该方法就会放弃运行的权利,于是 CPU 又去运行 A 或者其他线程了。

public class TestRunnable2 {

    public static void main(String[] args) {
        new Thread(new Task1(), "Task1").start();
        new Thread(new Task2(), "Task2").start();
    }
}

class Task1 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("\r\nreading...:" + Thread.currentThread().getName());
            System.out.println("running...:" + Thread.currentThread().getName());
        }

    }
}

class Task2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("\r\nreading...:" + Thread.currentThread().getName());
            Thread.yield();
            System.out.println("running...:" + Thread.currentThread().getName());
        }
    }
}

执行结果显示有三次 有可能放弃执行权发生,注意为什么是有可能,因为不排除在执行完 reading… Task2 后 Task2丢失执行权的可能。

在这里插入图片描述

join - 插入 or 加入

这个方法可以理解为,我在执行代码的时候,我的好朋友线程有更要紧的事情要处理,所以我使用此方法,让它先运行,等朋友线程运行完毕后,我再运行,多么美好的基情,好基友就该这样。 注意,join有三个重载 void join() 等待这个线程终止。 void join(long millis) 等待这个线程终止最多millis毫秒。 void join(long millis, int nanos) 等待最多 millis毫秒加上 nanos纳秒这个线程终止。

package com.example.demo;

public class TestRunnable3 {

    public static void main(String[] args) throws InterruptedException {
        Thread task = new Thread(new Task(), "Task");
        task.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("running is  " + Thread.currentThread().getName());
            task.join();
        }
    }
}

class Task implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("\r\nreading...:" + Thread.currentThread().getName());
            System.out.println("running...:" + Thread.currentThread().getName());
        }

    }
}

可以看到 在 main 线程刚开始运行时,就唤醒了 Task线程,并且直到 Task 线程执行完毕后,main 线程才继续执行

在这里插入图片描述

线程安全

举个例子,线程 A 和线程 B,同时需要打印 S 数组的最后一个元素后并清空集合,线程 A 首先拿到时间片也就是所谓的执行权,进行了判空操作通过,这时时间片失效,执行权落到了线程 B那里,线程 B 执行判空操作后顺利打印了元素并替换了空集合,这时线程 A 继续执行访问元素,代码抛出了角标越界异常。

public class Test4 {

    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("OK");
        Thread thread1 = new Thread(new DemoTask1(list));
        Thread thread2 = new Thread(new DemoTask2(list));

        thread1.start();
        thread2.start();
    }

}


class DemoTask1 implements Runnable {
    List<String> list;

    public DemoTask1(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        try {

            if (list.size() > 0) {
                Thread.sleep(1000);
                String s = list.get(0).toLowerCase();
                list.clear();
                System.out.println(s);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class DemoTask2 implements Runnable {
    List<String> list;

    public DemoTask2(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {

        if (list.size() > 0) {

            String s = list.get(0).toLowerCase();
            list.clear();
            System.out.println(s);
        }

    }
}

以上代码会出发线程安全问题,为什么会这样?这种情况出现的原因主要是因为这里访问了同一个线程共享的对象;因此就会出现这种类似“争抢”的情况发生;这就是所谓的线程不安全。

当线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致

临界资源: 共享资源(同一对象),一次允许一个线程使用,才可以保证其正确性。 原子操作:: 不可分割的多步操作,被视为一个整体,其顺序和步骤不能打乱或缺省(比如此处,A在执行过程中因时间片到期,却被B“插了队”,这就破坏了原子操作)

在这里插入图片描述

修复线程不安全——加锁

每个对象都有一个互斥标记锁,用来分配给线程的。只有拥有对象互斥锁标记的线程才能进入该对象加锁的同步代码块。线程退出同步代码块时会释放相应的互斥锁标记。

synchronized (临界资源对象){ //对临界资源加锁
    //代码(原子操作)
}

将 synchronized 添加到以上示例中发现,开再多的线程也不会出现线程安全问题了,就是这么简单的解决了线程安全问题。

class DemoTask1 implements Runnable {
    List<String> list;

    public DemoTask1(List<String> list) {
        this.list = list;
    }
    @Override
    public void run() {
        try {
            synchronized (list) {
                if (list.size() > 0) {
                    Thread.sleep(1000);
                    String s = list.get(0).toLowerCase();
                    list.clear();
                    System.out.println(s);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class DemoTask2 implements Runnable {
    List<String> list;
    public DemoTask2(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        synchronized (list) {
            if (list.size() > 0) {
                String s = list.get(0).toLowerCase();
                list.clear();
                System.out.println(s);
            }
        }

    }
}

总结

线程总共有以下几种状态

图片来自 Lukey Alvin 的博客

NEW (初始状态) 尚未启动的线程处于此状态。 RUNNABLE (运行状态) 在 Java 虚拟机中执行的线程处于此状态。 BLOCKED (阻塞状态) 被阻塞等待监视器锁定的线程处于此状态。 WAITING (无限期等待) 正在等待另一个线程执行特定动作的线程处于此状态。 TIMED_WAITING (有限期等待) 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 TERMINATED (终止状态) 已退出的线程处于此状态。