Loading

多线程总结

多线程相关总结

@

1. 多线程的定义

维基百科:

是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。

通俗点说就是同时执行多个任务。打个比方,CUP在运行QQ的时候也能收到微信消息。

CPU执行多线程的时候并不是同时执行的,而是分成多个时间片来执行,由于处理速度过快,就给人一种同时执行的感觉。这个后面还会提到

2. 创建一个多线程

这里使用Java来创建一个多线程

首先创建一个多线程要继承Thread

然后重写run方法,在run方法里面写你要这个线程做的事

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("这是一个新的线程");
    }
}

然后调用的这个线程的时候千万注意不能直接调用run方法,要调用start方法

public class Main {
    public static void main(String[] args) {
        final MyThread myThread = new MyThread();
        myThread.start();
    }
}

或者写简单点上面两个代码课以写成一个匿名类的形式

public class Main {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run(){
                System.out.println("这是一个新的线程");
            }
        }.start();

    }
}

再或者采用比较新的写法

public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("这是一个新的线程")).start();
    }
}

3. 线程同步

线程同步有一个很经典的例子,在数这个例子前要先介绍四个Thread类的常用方法

  • 计时等待

    Thread.sleep(),参数是毫秒级别,例如

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            final MyThread myThread = new MyThread();
            Thread.sleep(1000);
            myThread.start();
        }
    }
    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("这是一个新的线程");
        }
    
    }
    

    文字会停一秒后再打印

  • 线程等待Thread.join()

    这个就是等待这个线程结束再继续执行下一个任务,就相当于线程a调用线程b,在线程a里面调用b.join()这个方法,就会等b线程跑完再来继续运行a线程

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            final MyThread myThread = new MyThread();
            myThread.start();
            int i = 100;
            while(i > 0){
                System.out.println("Main线程");
                i--;
            }  
        }
    }
    
    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 100;
            while(i > 0){
                System.out.println("MyThread线程");
                i--;
            }
        }
    }
    

    这段代码运行可以发现它们的输出是夹杂在一起的,因为CPU执行一段时间Main线程就去执行MyThread线程,然后来来去去就夹杂在一起了,加上jion后:

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            final MyThread myThread = new MyThread();
            myThread.start();
            myThread.join();
            int i = 100;
            while(i > 0){
                System.out.println("Main线程");
                i--;
            }  
        }
    }
    
    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 100;
            while(i > 0){
                System.out.println("MyThread线程");
                i--;
            }
        }
    }
    

    加上join方法运行出来输出就不会混在一起了

  • 线程中断

    Thread.interrupt()用于中断某个线程,比如登qq的时候网不好登不上去就可以用到这个方法来退出登陆。

    中断线程不是无脑调用就会中断,这个需要在线程里面写好怎么处理这个中断信息,例如

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            final MyThread myThread = new MyThread();
            myThread.start();
            myThread.interrupt();
            myThread.join();
            System.out.println("运行结束");
        }
    }
    
    class MyThread extends Thread {
        @Override
        public void run() {
            int x = 0;
            while (! isInterrupted()){
                System.out.println(x++);
            }
        }
    }
    
    

    使用一个循环来一直检测是否有中断发生,如果有就退出循环,这个运行出来会在执行到myThread.join()之前中断线程。

    如果在之前加上一行,在主线程执行中断前把主线程暂停一毫秒再中断的话:

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            final MyThread myThread = new MyThread();
            myThread.start();
            Thread.sleep(1);
            myThread.interrupt();
            myThread.join();
            System.out.println("运行结束");
        }
    }
    
    class MyThread extends Thread {
        @Override
        public void run() {
            int x = 0;
            while (! isInterrupted()){
                System.out.println(x++);
            }
        }
    }
    
    

    运行出来的结果就是MyThread线程多执行了一毫秒的样子。

介绍完这三个方法,就可以介绍接下来这个很经典的多线程的例子了

public class Main {
    public static void main(String[] args) throws InterruptedException {
        final MyThread1 myThread1 = new MyThread1();
        final MyThread2 myThread2 = new MyThread2();
        myThread1.start();
        myThread2.start();
        myThread1.join();
        myThread2.join();
        System.out.println(num.count);
    }
}

class num{
    public static int count = 0;
}

class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++){
            num.count++;
        }
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++){
            num.count++;
        }
    }
}

这个代码非常的简单,但是运行起来会发现每次运行出来的结果都不是两千,总会小于2000,而且还少很多,这是因为CPU在执行这两个线程的时候(暂时不考虑Main线程,因为有join方法)是随机中断然后去执行另外一个线程,但是num.count++并不是原子操作(指的是不会被线程调度机制打断的操作)。在这行代码中,JVM要执行三条指令

ILOAD
IADD
ISTORE

然后在比如在线程1里面执行了ILOAD把数字加载出来了,但是还没有加就切换到了线程2,然后线程2加载的时候那个数字还和线程1刚刚加载的数字一样大然后2来执行自己的操作,但是1的操作并没有执行完,但是那块内存里面的值已经被线程2改变了,就会导致加值不上去,导致混乱。

解决方法就是加锁,给一个对象加锁,加上锁后在任意时刻最多只有一个线程能执行被锁上的代码块

加锁可以用synchronized关键字

给上述代码加锁:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        final MyThread1 myThread1 = new MyThread1();
        final MyThread2 myThread2 = new MyThread2();
        myThread1.start();
        myThread2.start();
        myThread1.join();
        myThread2.join();
        System.out.println(num.count);
    }
}

class num{
    public static int count = 0;
    public static final Object lock = new Object();
}

class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++){
            synchronized (num.lock) {
                num.count++;
            }
        }
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++){
            synchronized (num.lock) {
                num.count++;
            }
        }
    }
}

加上锁后输出就完全正确了,因为在线程1执行for循环里面synchronized的语句块的时候,线程2想要执行num.count++的操作,就必须要获取num.lock锁,但是这个锁在线程1执行自己的num.count++完之前是不会释放的,所以就不会出现上面分析的情况。

4. 线程安全

线程安全的定义:

线程安全程式设计中的术语,指某个函数函数库多执行绪环境中被调用时,能够正确地处理多个执行绪之间的公用变数,使程序功能正确完成。

假设有间银行只有 1000 元,而两个人同时提领 1000 元时就可能会拿到总计 2000 元的金额。为了避免这个问题,该间银行提款时应该使用互斥锁,即意味着针对同一个资源处理时,前一个人提领交易完成后才处理下一笔交易。但这种手法会使得效能降低。

——维基百科

简单来说就是 :当多个线程访问某个方法或者属性时,不论你怎么交替执行还是调用,我们的Mian线程不用做任何的同步处理,这个类的行为还是在我们的预想之中,那么我们就可以说这个类是线程安全的。

举个例子,一个简单的计数器程序:

package com.itranswarp.learnjava;

public class Counter {
    int count = 0;

    public void add() {
        synchronized (this) {
            count++;
        }
    }

    /**     也可以这样写,默认锁住当前实例对象
     *     public synchronized void add() {
     *          count++;
     *     }
     */

    public void sub() {
        synchronized (this) {
            count--;
        }
    }

    public int getCount() {
        synchronized (this) {
            return count;
        }
    }
}

第一次看到这个代码,我感觉到非常的奇妙,加锁居然能锁住自己当前的实例,这样就完美地把逻辑封装起来了,以这种方式加锁

  • 不需要借助外部对象来加锁

  • 外部调用时完全不用考虑线程同步问题

  • 可以有多个实例对象,由于锁是加在实例对象上的,获取锁的时候各个实例对象就不会相互干扰

那么这么一个精妙的类,我们就可以说它是线程安全的。

所有的类不做特殊说明都默认为非线程安全

如果一个静态方法用synchronized关键字所主,由于它没有实例对象,所以锁住的使它的反射,也就是方法名.class这个东西

5.死锁

先举个例子分析以下

public class DeadLock {
    int count = 0;

    public static void main(String[] args) throws InterruptedException {
        DeadLock deadLock = new DeadLock();
        // 实现Runnable接口来快速创建一个新线程并调用其start方法
        new Thread(new Runnable() {     // 线程1
            @Override
            public void run() {
                try {
                    deadLock.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {    //线程2
            @Override
            public void run() {
                deadLock.dev();
            }
        }).start();
    }
    

    public void add() throws InterruptedException {
        synchronized (lock.lock1){
            System.out.println("add()方法获取到lock1,正在期待获取到lock2...");
            Thread.sleep(1);// 在这里停一毫秒,确保dev()方法获取到lock2
            synchronized (lock.lock2){
                count += 1;
            }
            System.out.println("");
        }
    }
    
    public void dev(){
        synchronized (lock.lock2){
            System.out.println("dev()方法获取到lock2,正在期待获取到lock1...");
            synchronized (lock.lock1){
                count += 1;
            }
        }
    }
    
}

class lock {
    public static final Object lock1 = new Object();
    public static final Object lock2 = new Object();
}

运行结果是程序一直卡着运行不完

执行顺序

  • 线程Main开启lock1线程
  • lock1线程调用add方法然后获取lock.lock1
  • lock1线程休眠一毫秒
  • 线程Main开启lock2线程
  • lock2线程调用dev方法然后获取lock.lock2
  • lock1线程试图获取lock.lock2锁,但是这个锁被lock2占用,lock1等待lock2释放lock.lock2锁,只有lock1得到了锁lock.lock2才能释放lock.lock1锁和lock.lock2
  • lock2线程试图获取lock.lock1锁,但是这个锁被lock1占用,lock2等待lock1释放lock.lock1锁,只有lock2得到了锁lock.lock1才能释放lock.lock1锁和lock.lock2
  • 然后两个线程就互相等待,都想要对方手里的锁,但是谁也得不到,这样就会无限等下去,就产生了死锁

解决这个死锁只需要控制好获取锁的顺序,使湖区锁的顺序一致,就能避免这个死锁了,即把dev方法改为:

public void dev(){
    synchronized (lock.lock1){// 获取锁的顺序跟add方法一致就行
        System.out.println("dev()方法获取到lock2,正在期待获取到lock1...");
        synchronized (lock.lock2){
            count += 1;
        }
    }
}

文中部分参考

posted @ 2021-08-14 20:37  [X_O]  阅读(48)  评论(0编辑  收藏  举报