多线程知识总结

1. 进程与线程

进程:是操作系统中的各个程序,是资源分配的基本单位,举例:QQ.exe,分配内存、端口号

线程:是执行程序的基本单位,也就是说程序中的代码是由线程执行的,一个进程中包含一个或多个线程,可以并行执行。

1. 多线程的重要性

多线程是基础,基础到什么程度?基础到如果不会多线程,那么最简单的CRUD都写不好。

具个例子:现在有一个Person类,有name属性,保存时要求name唯一,

这时如果两个用户同时提交且名字一样,那么后台就会有两个线程同时执行 create 方法,最终导致数据库存入两条重复的数据。

或许你会说在数据库表中增加唯一约束,但是如果该表的数据只能逻辑删除,这样就是有问题的。

所以,只有学好多线程、锁,这些东西,才能写好代码。

2. 创建线程的两种方式

/**
 * 创建线程的方式:
 *  1. 继承 Thread 类,复写 run 方法
 *  2. 实现 Runnable 接口,推荐使用,因为java语言规范,不能多继承,但是允许多实现
 *  3. Callable,需要被 FutureTask 封装,通过get方法获取返回值,而 FutureTask 本质上还是实现了Runnable
 *  4. 线程池,本质上还是第二种
 *
 */

class T1 extends Thread {
    @Override
    public void run() {
        System.out.println("这是创建线程的第1种方式");
    }
}


class T2 implements Runnable {

    @Override
    public void run() {
        System.out.println("这是创建线程的第2种方式,推荐使用");
    }
}

class T3 implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("这是创建线程的Callable方式");
        return "thread";
    }
}

public class T01_Create_thread {

    public static void main(String[] args) {
        //1. 继承 Thread 类
        new T1().start();
        //2. 实现 Runnable 接口
        new Thread(new T2()).start();
        //3. Callable
        FutureTask<String> task = new FutureTask<>(new T3());
        new Thread(task).start();
        System.out.println(task.get());
    }
}

 

为什么写了4种?

因为网上有种说法,把 Callable和线程池 各自列为一种创建线程的方式,

但是我们需要知道其实它们的在本质上都是第二种

3. 线程状态

该知识点,了解就行,Thread类中有State枚举

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程创建后,且调用了start()方法。该状态的线程位于可运行线程池中,等待获取CPU的使用权,

此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3. 阻塞(BLOCKED):表示线程等待获取锁。

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(唤醒或打断)。

5. 超时等待(TIMED_WAITING):跟WAITING不同,可在指定时间后自己运行,如果等待时释放锁,超时后阻塞于锁。

6. 终止(TERMINATED):表示该线程已经执行完毕。

状态切换时常用方法

该知识点,了解就行,Thread类中有State枚举

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程创建后,且调用了start()方法。该状态的线程位于可运行线程池中,等待获取CPU的使用权,

此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3. 阻塞(BLOCKED):表示线程等待获取锁。

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(唤醒或打断)。

5. 超时等待(TIMED_WAITING):跟WAITING不同,可在指定时间后自己运行,如果等待时释放锁,超时后阻塞于锁。

6. 终止(TERMINATED):表示该线程已经执行完毕。

状态切换时常用方法

start()

启动一个线程

run()

线程需要执行的代码,run方法结束,该线程结束

sleep(long millis)

线程休眠,但不释放锁

join()

等待该线程结束,可以让线程顺序执行

wait()/notify()/notifyAll()

wait()使当前线程等待,前提是 必须先获得锁,一般配合synchronized 关键字使用

只有当 notify/notifyAll() 被执行时候,才会唤醒该线程继续执行,直到执行完synchronized 代码块或是再次遇到wait() 

notify/notifyAll() 的执行只是唤醒等待的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁

 代码演示

/**
 * 工具类
*/
public class ThreadHelper {
    public static void sleep(long millisecond) {
        try {
            Thread.sleep(millisecond);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 状态切换时常用方法
 * start()
 *      启动一个线程
 * run()
 *      线程需要执行的代码,run方法结束,该线程结束
 * sleep(long millis)
 *      线程休眠,但不释放锁
 * join()
 *      等待指定的线程结束,可以让线程顺序执行
 * wait()/notify()/notifyAll()
 *      wait()使当前线程等待,前提是 必须先获得锁,一般配合synchronized 关键字使用
 *      只有当 notify/notifyAll() 被执行时候,才会唤醒该线程继续执行
 *
 *      notify/notifyAll() 的执行只是唤醒等待的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。
 *      所以尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
 */
public class T02_Thread_status02 {

    public static void main(String[] args) throws InterruptedException {
        waitAndNotify();
    }

    /**
     * wait/notify/notifyAll的使用
     */
    private static void waitAndNotify() throws InterruptedException {
        Object t = new Object();

        new Thread(() -> {
            synchronized (t){
                try {
                    System.out.println("线程1开始等待。。。。。。");
                    t.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程1结束。。。。。。");
        }).start();

        new Thread(() -> {
            
            synchronized (t){
                try {
                    System.out.println("线程2开始等待。。。。。。");
                    t.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程2结束。。。。。。");
        }).start();

        ThreadHelper.sleep(2000);

        new Thread(() -> {
            synchronized (t){
                System.out.println("唤醒线程。。。。。。");
                t.notify();//只会唤醒等待线程中的一个,具体是哪一个,跟JVM有关,hotspot是顺序唤醒,唤醒的线程在当前线程释放锁后,继续运行
//                t.notifyAll();
                ThreadHelper.sleep(2000);
            }
        }).start();
    }
}

特殊情况下的notify

/**
 * 特殊情况下,notify会唤醒所有
 */
class T extends Thread {
    @Override
    public void run() {
        synchronized (this){
            this.notify();
            System.out.println(this.hashCode()+"----"+this);
        }
    }
}

public class T02_Thread_status03 {

    public static void main(String[] args) throws InterruptedException {
        new T02_Thread_status03().waitAndNotify();
    }

    /**
     * wait/notify/notifyAll的使用
     */
    private void waitAndNotify() throws InterruptedException {
        /**
         * 1. 锁是一个 线程
         */
        T t = new T();

//        Thread t = new Thread(() ->{
//            synchronized (this){
//                System.out.println(this.hashCode()+"----"+this);
//                this.notify();
//            }
//        });

        new Thread(() -> {
            System.out.println("线程1开始等待。。。。。。");
            synchronized (t){
                try {
                    t.wait();//释放锁
                    System.out.println(t.hashCode()+"----"+t);
                    System.out.println("线程1结束。。。。。。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            System.out.println("线程2开始等待。。。。。。");
            synchronized (t) {
                try {
                    t.wait();
                    System.out.println(t.hashCode()+"----"+t);
                    System.out.println("线程2结束。。。。。。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        ThreadHelper.sleep(2000);
        t.start();
    }
}

4. 一些常见问题

1. 多个线程顺序执行

  • 使用join方法
  • 定义一个共享变量,各个线程根据变量执行

2. 停止线程

停止线程的最好方式是让线程正常结束

  • 声明一个变量,设置一个开关
  • interrupt方法,interrupt()并不会终止线程!只是将线程的中断标记设为true 
    • 如果线程在阻塞、睡眠、等待,会抛出InterruptedException
/**
 * 一些常见问题
 */
public class T03_Other {

    public static void main(String[] args) throws InterruptedException {
        //1. 多个线程之间顺序执行
        Thread t1 = new Thread(() -> System.out.println("线程1执行。。。。。。"));
        Thread t2 = new Thread(() -> System.out.println("线程2执行。。。。。。"));
        Thread t3 = new Thread(() -> System.out.println("线程3执行。。。。。。"));

        t1.start();
        t1.join();
        t2.start();
        t2.join();
        t3.start();
        t3.join();
//
//        System.out.println("执行完毕");
        //join可以阻塞调用线程的执行(本例中调用线程是主线程),等指定线程执行结束后才继续执行调用线程




        /**
         * 2. 停止一个线程
         *    停止线程的最好方式是让线程正常结束
         *    a. 在循环中声明一个变量,设置一个开关
         *    b. interrupt 方法,但它并不会终止线程!只是将线程的中断标记设为true
         *       如果线程在阻塞、睡眠、等待,会抛出InterruptedException
         */
//        Thread t = new Thread(() -> {
//            for (int i = 0; i<10; i++) {
                if(Thread.currentThread().isInterrupted()){
                    break;
                }
//                System.out.println(i);
//                ThreadHelper.sleep(1000);
//            }
//        });
//        t.start();
//        Thread.sleep(1000);
//        t.interrupt();
//        t.isInterrupted();// true:表示当前线程被打断了
//        Thread.interrupted();//查询当前线程是否被打断,并重置打断标志,其实里面调用的还是isInterrupted方法
    }
}

5. synchronized

1. 字符串加锁

不建议对字符串加锁,字符串比较特殊,一般情况下在内存中只有一份儿,两个线程分别对同一个字符串加锁,非常容易产生阻塞,甚至是死锁。而且如果用法不对,加锁毫无效果。

/**
 * 字符串加锁
 * 不建议对字符串加锁,字符串比较特殊,一般情况下在内存中只有一份儿,两个线程分别对同一个字符串加锁,非常容易产生阻塞,甚至是死锁。而且如果用法不对,加锁毫无效果。
 */
public class T04_Synchronized {

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            show("feng");
        }).start();

        new Thread(() -> {
            show(new String("feng"));
        }).start();
    }

    public static void show(String str){
        synchronized (str){
//        synchronized (str.intern()){ // 如果字符串池中存在当前字符串, 就会直接返回当前字符串. 如果没有, 会将字符串放入池中后, 再返回
            System.out.println(str);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2. 锁升级过程

待完善

简单过程

  • 第一次加锁,偏向锁,记录Thread Id
  • 另一个线程来,发现Thread Id 不同,锁升级:轻量级锁,又称 自旋锁
  • 自旋一定次数后仍然抢不到锁,升级为重量级锁,线程挂起,等待

轻量级和重量级锁的使用,轻量级自旋时很消耗cpu

如果线程数少,而且运行速度较快,适合轻量级锁,反之使用重量级锁

2. 线程3个特性

1. 可见性

在java中,每一个线程都有一块工作内存,其中存放着主内存中的变量值得拷贝,当线程执行时,它在自己的工作内存区中操作这些变量。

代码:

/**
 * 证明工作内存的存在
 */
class Vo implements Runnable {

    boolean bool = true;
    @Override
    public void run() {
        System.out.println("start......");
        while (bool){

        }
        System.out.println("end......");
    }
}

public class T05_Volatile {

    public static void main(String[] args) throws InterruptedException {
        Vo vo = new Vo();
        new Thread(vo).start();

        Thread.sleep(1000L);
        vo.bool = false;
    }
}
//运行该代码,会发现死循环,“System.out.println("end......");”这句代码永远也不会执行
//这时候只要在bool变量前加上volatile修饰符,程序就能正常结束

 

        volatile 使变量在多个线程中可见,当一个线程修改变量后,强制其他线程到主内存中读取变量值,性能比synchronized强,不会阻塞。但是不具备原子性,不适当的使用,在CPU层面上极有可能造成计算速度降低

代码:


/**
 * 不适当的使用volatile会造成速度降低
 */
class Demo {
    long a;
//    volatile long a;
}
public class Volatile03 {

    public static void main(String[] args) throws InterruptedException {
        Demo[] arr = new Demo[]{new Demo(),new Demo()};
        long start = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                //第1个线程修改数组中的第一个元素
                arr[0].a = i;
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for(int i =0;i<100000000;i++){
                //第2个线程修改数组中的第二个元素
                arr[1].a = i;
            }
        });
        t2.start();

        t1.join();t2.join();
        System.out.println(System.currentTimeMillis()-start);
    }

 

以上代码运行时间:220毫秒左右,

对代码进行修改,把变量a加上 volatile,运行时间:3000毫秒左右。

想知道原因,得先弄明白以下几个东西:

CPU缓存

工作内存 本质上就是 CPU缓存 ,是 CPU与内存之间的临时数据区

为什么需要CPU缓存?

  • 解决CPU运行速度与内存读写速度不匹配的矛盾——缓存的速度比内存的速度快多了。
  • CPU往往需要重复处理相同的数据、重复执行相同的指令,如果这部分数据、指令CPU能在CPU缓存中找到,CPU就不需要从内存或硬盘中再读取数据、指令,从而提高运行速度。

CPU缓存分为3级:L1一级缓存、L2二级缓存、L3三级缓存,它们的作用都是作为CPU与主内存之间的高速数据缓冲区,L1最靠近CPU核心;L2其次;L3再次。

速度方面:L1最快、L2次快、L3最慢;

大小方面:L1最小、L2较大、L3最大。

CPU会先在L1中寻找需要的数据,找不到再去L2,还找不到再去L3,L3都没有那就只能去主内存找了。

一级缓存其实还分为一级数据缓存(Data Cache,L1d-Cache)和一级指令缓存(Instruction Cache,l1i-Cache),分别用于存放数据及指令,两者可同时被CPU访问,减少了CPU多核心、多线程争用缓存造成的冲突,提高处理器性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值