【java基础】多线程

152 阅读20分钟

多线程

一、进程的概念

1.1 程序

程序是在系统中安装的一系列的文件,有特定的作用。是静止的内容。

1.2 进程

当打开某个程序的文件夹,双击程序的启动文件(一般windows上该文件的后缀名为exe),此时会启动该程序,启动后会打开窗口或者在托盘中显示相应图标。

此时打开进程窗口,会看到该程序对应的进程。

一般情况下,一个程序启动后会产生一个进程。正在运行的程序称为进程。

注意:有可能一个程序运行后产生多个进程。

注意:单核CPU在任何时间点上只能运行一个进程,微观串行,宏观并行。

串行和并行:

串行:一个一个的进行。

并行:多个一起进行。

目前绝大部分系统都是抢占式执行。即抢占CPU的执行时间。

二、线程的概念

线程是一个轻量级的进程,也可以说线程是一个弱化版的进程。一个进程中可以包含多个线程,称为多线程。

多个线程之间一样是微观串行,宏观并行。抢占CPU的执行时间,谁抢到谁执行。

注意: 如果是多核CPU有可能并行。并非一个线程执行完毕后再执行另一个线程,是交替执行,才能达到宏观并行。

Main线程:也成为主线程,当启动Java程序,使用main方法时,会创建一个main线程。

一个进程至少有一个线程(主线程),也可以有多个线程。

三、进程与线程的区别

经典面试题:

  • 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
  • 一个程序一般有一个进程,一个进行至少有一个线程,可以有多个线程。
  • 进程之间一般不能共享数据,但是线程之间可以共享数据。

四、线程的组成

  • CPU的时间片,线程执行必须抢占时间片才能运行。

  • 运行数据:

    • 栈空间:运行过程中使用的局部变量,每个线程都有独立的栈空间。
    • 堆空间:运行过程中使用的对象,多个线程可以共享对象
  • 逻辑代码

五、线程的创建和启动

5.1 线程的创建

线程的创建常用两种方式:继承Thread类和实现Runnable接口。

5.1.1 继承Thread类
public class MyThread extends Thread{
    @Override
    public void run() {
        // currentThread()用来获取当前线程
        // 得到当前线程名称
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + "=======" + i);
        }
    }
}
5.1.2 实现Runnable接口

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // currentThread()用来获取当前线程
        // 得到当前线程名称
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + ">>>>>>>" + i);
        }
    }
}

经典面试题:

继承Thread类和实现Runnable接口区别?

  • 继承Thread类就不能继承其他类,实现Runnable接口还可以继承其他类,有更好的扩展性。
  • 继承Thread类的对象可以直接启动,而实现Runnable接口其实仅仅是重写了线程执行过程中的业务逻辑代码,要想启动线程还需要创建一个线程对象。
5.2 线程的启动

线程的启动需要使用start()方法。


public class TestMain {
    public static void main(String[] args) {
        // 创建继承自Thread类的对象
        MyThread t1 = new MyThread();
        t1.start();
        
        // 创建实现了Runnable接口的对象
        MyRunnable r = new MyRunnable();
        Thread t2 = new Thread(r);
        t2.start();
    }
}

多线程交替执行,理论上来说会比单线程性能有提升,但是如果有共享数据会有线程安全的风险。

多线程是利用CPU的空闲时间执行,并非越多越好,还是要考虑CPU的性能。

经典面试题:

start方法和run方法的区别?

  • run方法中写的是线程执行的业务逻辑代码,如果直接调用run方法,会将业务逻辑在当前线程中执行,并不会开启多线程。
  • start方法,表示开启多线程,进入线程的执行过程,抢占CPU执行时间,并执行run方法中的业务逻辑。

五、线程的基本状态

基本:

  • 新建状态(初始状态):使用new关键字创建线程。
  • 就绪状态:调用start方法后进入就绪状态,该状态可以开始抢占时间片。
  • 运行状态:当抢占到时间片,进入运行状态,执行业务。当时间片到期,如果业务没有执行完毕,进入就绪状态。
  • 终止状态:业务执行完毕,或者main执行完毕,进入终止状态,释放时间片。

注意:调用start方法后,线程只是进入了就绪状态,并没有立即执行。

六、线程执行过程中的常用方法

6.1 sleep

休眠。不会抢占时间片,直到休眠时间结束。时间结束后进入就绪状态。


public class MyThread extends Thread{
    @Override
    public void run() {
        // currentThread()用来获取当前线程
        // 得到当前线程名称
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + "=======" + i);
            if(i == 30) {
                try {
                    // 单位:毫秒
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } 
            }
        }
    }
}
6.2 yield

放弃当前时间片,回到就绪状态,进入下次的时间片竞争。


public class MyThread extends Thread{
    @Override
    public void run() {
        // currentThread()用来获取当前线程
        // 得到当前线程名称
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + "=======" + i);
            if(i == 30) {
                Thread.yield();
            }
        }
    }
}
6.3 join

将其他的线程合并到当前线程中。(插队)


public class TestMain1 {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "=======" + i);
                }
            }
        };
        Thread t1 = new Thread(r1);
        
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + ">>>>>>" + i);
                    if(i == 30) {
                        try {
                            t1.join();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };  
        Thread t2 = new Thread(r2);
        
        t1.start();
        t2.start();
    }
}

应用场景:多线程下载,所有线程下载完毕才能合成一个下载内容,可以在合成的代码之前将其他线程都合并进来,以实现所有线程下载完毕后才执行合成。

七、线程的状态(等待)

等待:

  • sleep方法调用后,线程进入限期等待,当时间到期后,线程进入就绪状态。
  • join方法调用后,线程进入无限期等待,当加入的线程执行完毕后,当前线程才进入就绪状态。
  • wait方法调用后,线程会进入无限期(限期)等待,直到被notify或notifyAll唤醒(超时到期)后,线程才会进入就绪状态。

八、线程安全

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

// 售票员卖票问题
public class Seller extends Thread{
    private static int ticket = 10;
    private String name;
    
    public Seller(String name) {
        super();
        this.name = name;
    }
​
    @Override
    public void run() {
        while(ticket > 0) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(ticket > 0) {
                ticket--;
                System.out.println(name + "卖出一张票, 还剩下"+ticket+"张票");
            }
        }
    }
}

public class TestMain2 {
    public static void main(String[] args) {
        Seller s1 = new Seller("龙修");
        s1.start();
        Seller s2 = new Seller("文达");
        s2.start();
        Seller s3 = new Seller("国栋");
        s3.start();
    }
}

九、线程同步

同步:其他线程等待当前线程执行。

异步:多个线程同时执行。

将操作共享数据改变时的代码进行加锁,加锁后此段代码执行时,其他线程如果执行到此代码时需要等待。

语法:

同步代码块:

synchronized(加锁的对象){

​ // 临界资源修改的代码

}


public class Seller extends Thread{
    private static Integer ticket = 10;
    private String name;
    
    public Seller(String name) {
        super();
        this.name = name;
    }
​
    @Override
    public void run() {
        while(ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (ticket) {
                if(ticket > 0) {
                    ticket--;
                    System.out.println(name + "卖出一张票, 还剩下"+ticket+"张票");
                }
            }
        }
    }
}

同步方法:

语法:

public synchronized void method(){

}

public synchronized static void method(){

}

同步方法与同步代码块的区别:

  • 同步代码块比较灵活,只锁部分代码。而同步方法锁整个方法。
  • 实例方法锁的是this对象
  • 静态方法锁的是当前类.class

十、线程的状态(阻塞)

阻塞状态:当线程在执行过程中,执行到线程同步代码时,需要持有锁,此时,只会有一个线程持有锁,其他没有持有锁的线程被迫进入等待抢锁,称为阻塞状态。

十一、线程的通信

使用wait和notify\notifyAll方法来在多个线程之间进行通信。

wait():进入等待直到被唤醒

wait(long timeout):进入等待,设置超时时间,如果在没有到时间时,可以被唤醒,如果时间到还没被唤醒,会自动醒来。

notify():唤醒被wait的线程,如果有多个线程被同一个对象wait,一次notify只会随机唤醒一个线程。

notifyAll():唤醒所有被一个对象wait的线程。

注意:使用一个对象的wait或者notify方法,必须在该对象加锁的代码中。


public class Test {
    private static final Object obj = new Object();
    
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "=====" + i);
                    if(i == 30) {
                        synchronized (obj) {
                            try {
                                // 单位:毫秒,在2000毫秒内可以唤醒,如果没有唤醒,会自动醒来
//                              obj.wait(2000); 
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        };
        
        Thread t1 = new Thread(r1);
        t1.start();
        Thread t = new Thread(r1);
        t.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                    if(i == 300) {
                        synchronized (obj) {
                            // 每次notify只会随机唤醒一个
                            // notifyAll才会唤醒所有的
//                          obj.notify();
//                          obj.notify();
                            obj.notifyAll();
                        }
                    }
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
}

经典面试题:sleep和wait的区别?

  • sleep是限期等待,只能等待时间到期才会进入就绪状态。wait如果不设置过期时间,只能等待被唤醒,如果设置过期时间,可以在时间内被唤醒,也可以等到时间超时自动醒来。
  • wait会释放锁,sleep不释放锁。

sleep不释放锁的案例:


public class Test2 {
    private static final Object obj = new Object();
    
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "=====" + i);
                    if(i == 30) {
                        synchronized (obj) {
                            try {
                                // 会持有锁
                                //Thread.sleep(10000);
                                // 会释放锁
                                obj.wait(10000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        };
        
        Thread t1 = new Thread(r1);
        t1.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                    if(i == 200) {
                        synchronized (obj) {
                            System.out.println(">>>>>>>====" + i);
                        }
                    }
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
}

十二、死锁

当A线程持有a锁,并且需求b锁,而B线程持有b锁,并需求a锁,且都不释放各自的锁时,产生死锁。


public class Test3 {
    public static void main(String[] args) {
        Boy boy = new Boy();
        Girl girl = new Girl();
        boy.start();
        girl.start();
    }
​
    private static class Boy extends Thread{
        @Override
        public void run() {
            synchronized (a) {
                System.out.println("男生抢到a");
//              try {
//                  a.wait();
//              } catch (InterruptedException e) {
//                  e.printStackTrace();
//              }
                synchronized (b) {
                    System.out.println("男生抢到b");
                    System.out.println("男生吃东西");
                }
            }
            
        }
    }
    
    private static class Girl extends Thread{
        @Override
        public void run() {
            synchronized (b) {
                System.out.println("女生抢到b");
                synchronized (a) {
                    System.out.println("女生抢到a");
                    System.out.println("女生吃东西");
//                  a.notify();
                }
            }
            
        }
    }
    
    // 两根筷子
    public static final Object a = new Object();
    public static final Object b = new Object();
}

十三、生产消费模式

13.1 设计模式

解决某一类业务问题,经过实践证明,行之有效的经验的总结,称为设计模式。

90年代初,4人组总结并推出书。23种设计模式,将当时软件行业遇到问题类型分为3大类,共23种。

创建型模式:创建对象过程中使用的模式。一共5种,例如:单例模式、工厂模式、原型模式等。

结构型模式:多个对象共同形成一种新的结构。一共7种,例如:门面模式(装饰模式)、桥接模式、适配器模式。

行为型模式:多个对象之间产生行为。一共11种,例如:命令模式、监视模式、迭代模式、调停模式等。

13.2 生产消费模式

若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。


public class Car {
    private String name;
​
    public Car(String name) {
        super();
        this.name = name;
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    @Override
    public String toString() {
        return "Car [name=" + name + "]";
    }
}

public class Producer extends Thread{
    private Storage storage;
    
    public Producer(Storage storage) {
        super();
        this.storage = storage;
    }
​
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 15; i++) {
            Car car = new Car(name + "===" + i);
            storage.add(car);
        }
    }
}

public class Customer extends Thread{
    private Storage storage;
    
    public Customer(Storage storage) {
        super();
        this.storage = storage;
    }
​
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            storage.out();
        }
    }
}

public class Storage {
    private Car [] cars = new Car[6];
    private int size = 0;
    
    public synchronized void add(Car car) {
        while(size >= 6) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        cars[size++] = car;
        System.out.println("添加了一辆汽车," + car);
        this.notifyAll();
    }
    
    public synchronized void out() {
        while(size <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Car car = cars[--size];
        System.out.println("消费了一辆汽车," + car);
        cars[size] = null;
        this.notifyAll();
    }
}

十四、线程的中止

让线程中止的方式:

  • stop(不推荐使用),会导致程序出现未知的问题,数据没有及时保存,线程不正常退出。
  • 设置标识符,在程序的执行过程中,通过判断标识符来确定是否停止。让线程在不满足条件下正常结束。
  • interrupt 系统提供的标识。
14.1 stop

public class Test4 {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; ; i++) {
                    System.out.println(name + "=====" + i);
                }
            }
        };
        
        Thread t1 = new Thread(r1);
        t1.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                    if(i == 200) {
                        t1.stop();
                    }
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
}
14.2 设置标识符

下面的案例,能够演示出效果,但是有瑕疵。


public class Test4 {
    public static boolean b = false;
    
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; ; i++) {
                    System.out.println(name + "=====" + i);
                    if(b) {
                        break;
                    }
                }
            }
        };
        
        Thread t1 = new Thread(r1);
        t1.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                    if(i == 200) {
                        b = true;
                    }
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
}
14.3 interrupt

public class Test4 {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; ; i++) {
                    System.out.println(name + "=====" + i);
                    // 判断是否被打断
                    if(Thread.interrupted()) {
                        break;
                    }
                }
            }
        };
        
        Thread t1 = new Thread(r1);
        t1.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                    if(i == 200) {
                        // 打断上面线程的执行
                        t1.interrupt();
                    }
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
}

当线程正在sleep或者wait时,打断该线程会报错。


public class Test4 {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; ; i++) {
                    System.out.println(name + "=====" + i);
                    if(i == 30) {
                        try {
                            Thread.sleep(15000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            // 当sleep或者wait时,遇到被打断,说明需要中止线程
                            // 那么也应该中止
                            break;
                        }
                    }
                    if(Thread.interrupted()) {
                        break;
                    }
                }
            }
        };
        
        Thread t1 = new Thread(r1);
        t1.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                    if(i == 200) {
                        t1.interrupt();
                    }
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
}

十五、守护线程

守护线程的作用是守护其他线程执行,如果其他线程都结束了,守护线程会自动结束。JVM也是一个守护线程。


public class Test4 {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; ; i++) {
                    System.out.println(name + "=====" + i);
                }
            }
        };
        
        Thread t1 = new Thread(r1);
        // 设置为守护线程
        t1.setDaemon(true);
        t1.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
}

十六、线程的优先级

可以设置线程的抢占CPU的时间片的优先程度,默认为5,最高为10,最低为1。


public class Test5 {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 200; i++) {
                    System.out.println(name + "=====" + i);
                }
            }
        };
        
        
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 200; i++) {
                    System.out.println(">>>>>>>" + i);
                }
            }
        };
        
        
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.setPriority(Thread.MAX_PRIORITY); // 10
        t2.setPriority(Thread.MIN_PRIORITY); // 1
        
        t1.start();
        t2.start();
    }
}

十七、volatile关键字用法

volatile可以用来修饰属性,表示该属性具备有可见性,即多线程操作时,每次要读取该属性,必须加载它最新的变化。


public class Test6 {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.start();
        
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 302; i++) {
                    System.out.println(">>>>>>>" + i);
                    if(i == 300) {
                        t1.a = "b";
                    }
                }
            }
        };
        
        Thread t2 = new Thread(r2);
        t2.start();
    }
    
    private static class MyThread1 extends Thread{
        public volatile String a = "a";
        
        @Override
        public void run() {
            System.out.println("程序开始执行");
            while(a.equals("a")) {
                
            }
            System.out.println("程序执行完毕");
        }
    }
}

上面的案例,当没有使用volatile关键字时,即使属性a的值被另一个线程改变了,由于当前线程没有空闲时间,无法做到最基本的值同步,所以,线程不会结束。

但是如果在while循环中加入一个打印输出的代码,就可以执行完毕,因为打印输出对于CPU来说有足够的空闲时间,而线程会在有足够空闲时间的情况下,尽量保证数据的一致性。

当使用volatile关键字时,表示每次读取属性a的值时,都会加载最新的变化。

经典面试题:volatile关键字的作用,以及与synchronized的区别?

  • volatile保证属性的可见性,但是并不能保证线程安全,因为不能保证属性的互斥性。
  • synchronized实现了线程安全,保证了属性的互斥性和可见性。
  • 互斥性是指如果一个线程在操作,其他的线程需要等待,不能操作。
  • 可见性是指如果一个线程改变了属性的值,其他线程在访问时需要去加载最新的变化。

十八、线程池

池本质是一个集合,用来存放和管理多个对象,避免频繁创建和销毁对象。

线程池就是管理线程的一个池。

池的管理,可以设置大小,最低的数量等。

将任务(业务)提交给线程池,由线程池分配分配线程和任务的执行,如果线程用完,任务会等待,直到有空闲的线程来执行该任务。


public class TestPool {
    public static void main(String[] args) {
        // 创建有4个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(4);
        // 创建无上限的线程池,理论最大值为Integer的最大值
//      ExecutorService service = Executors.newCachedThreadPool();
        
        // 定义一个任务
        Runnable r = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "----" + i);
                }
            }
        };
        
        // 将任务交给线程池去执行
        service.submit(r);
        service.submit(r);
        service.submit(r);
        service.submit(r);
        service.submit(r);
        service.submit(r);
        
        // 关闭线程池
        service.shutdown();
    }
}

十九、Callable接口和Future接口

Callable接口类似Runnable接口,也能够处理业务,但是其有返回值。

Future接口用来接收Callable接口的返回值,使用get方法获取返回值,get方法是同步的,必须等待线程执行完毕才能获取结果,获取过程中,可能会因为线程执行异常导致获取失败。

案例:通过两个线程分别计算150和51100的和,然后计算总和。


public class TestPool2 {
    public static void main(String[] args) {
        // 创建有4个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(4);
        // 创建无上限的线程池,理论最大值为Integer的最大值
//      ExecutorService service = Executors.newCachedThreadPool();
        
        // 求1~50之和
        Callable<Integer> r1 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 50; i++) {
                    sum += i;
                    Thread.sleep(100);
                }
                System.out.println("1~50之和为:" + sum);
                return sum;
            }
        };
        
        // 求51~100之和
        Callable<Integer> r2 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 51; i <= 100; i++) {
                    sum += i;
                }
                System.out.println("51~100之和为:" + sum);
                return sum;
            }
        };
        
        // 将任务交给线程池去执行
        Future<Integer> f1 = service.submit(r1);
        Future<Integer> f2 =service.submit(r2);
        
        // 通过get方法获取结果
        try {
            // 可能没有计算出结果,所以需要处理异常
            System.out.println("结果为:" + (f1.get() + f2.get()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        
        // 关闭线程池
        service.shutdown();
    }
}

二十、Lock锁

20.1 Lock接口

在JDK1.5加入,与synchronized作用相似,但是lock接口更灵活,可以手动控制加锁和解锁,甚至可以尝试是否加锁。

注意:解锁的代码最好写在finally中。

20.2 重入锁(公平锁)

当创建锁对象时,使用无参构造或者在构造时传入false,表示使用重入锁,不公平,默认。

当创建锁对象时,使用构造时传入true,表示使用公平锁。公平是指先等待获取锁的线程会在其他线程释放锁时优先持有锁。在特殊的业务场景使用,保证公平,但会消耗更多的性能。


public class Seller extends Thread{
    private static Integer ticket = 10;
    private static ReentrantLock lock = new ReentrantLock();
    private String name;
    
    public Seller(String name) {
        super();
        this.name = name;
    }
​
    @Override
    public void run() {
        while(ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            try {
                // 加锁
                lock.lock();
                if(ticket > 0) {
                    ticket--;
                    System.out.println(name + "卖出一张票, 还剩下"+ticket+"张票");
                }
            } finally {
                // 解锁
                lock.unlock();
            }   
​
        }
    }
}

public class TestMain2 {
    public static void main(String[] args) {
        Seller s1 = new Seller("张三");
        s1.start();
        Seller s2 = new Seller("李四");
        s2.start();
        Seller s3 = new Seller("王五");
        s3.start();
    }
}

二十一、读写锁

普通的锁会不管读写,都会互斥,而如果所有的线程都在读时,其实没有线程安全的问题,当读操作远大于写操作时,可以使用读写锁,读写锁的特点是:只有线程有写时,才会互斥,如果都是读,会一起执行。


public class MyClass {
    private int value;
    // 普通重入锁
//  private static final ReentrantLock lock = new ReentrantLock();
    // 读写锁
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static final ReadLock readLock = lock.readLock();
    private static final WriteLock writeLock = lock.writeLock();
​
    public int getValue() {
//      lock.lock();
        readLock.lock();
        try {
            Thread.sleep(1000);
            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
//          lock.unlock();
            readLock.unlock();
        }
        return 0;
    }
​
    public void setValue(int value) {
//      lock.lock();
        writeLock.lock();
        try {
            Thread.sleep(1000);
            this.value = value;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
//          lock.unlock();
            writeLock.unlock();
        }
    }
}

public class TestMain {
    public static void main(String[] args) {
        final MyClass c = new MyClass();
        // 定义读任务
        Runnable r = new Runnable() {
            @Override
            public void run() {
                c.getValue();
            }
        };
        
        // 定义写任务
        Runnable w = new Runnable() {
            @Override
            public void run() {
                c.setValue(1);
            }
        };
        
        // 创建线程池
        ExecutorService service = Executors.newFixedThreadPool(20);
        // 得到线程开始执行的系统时间
        long startTime = System.currentTimeMillis();
        
        // 加入2个写线程
        for (int i = 0; i < 2; i++) {
            service.submit(w);
        }
        // 加入18个读线程
        for (int i = 0; i < 18; i++) {
            service.submit(r);
        }
        // 关闭线程池
        service.shutdown();
        // 当线程池内的任务没结束时,会一直卡住
        while(!service.isTerminated()) {
            
        }
        // 得到线程执行结束的系统时间
        long endTime = System.currentTimeMillis();
        System.out.println("一共使用了"+(endTime-startTime)+"毫秒");
    }
}

二十二、线程安全的集合

22.1 Collections类中提供的线程安全集合的处理方法

静态方法:synchronizedXxx() 重写对应接口的方法,然后在方法中加锁后调用原来的方法。

在JDK1.2添加,性能与老版本的Vector之类的没有提升,只是接口统一而已。


public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;
​
    final List<E> list;
​
    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }
    SynchronizedList(List<E> list, Object mutex) {
        super(list, mutex);
        this.list = list;
    }
​
    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {return list.equals(o);}
    }
    public int hashCode() {
        synchronized (mutex) {return list.hashCode();}
    }
​
    public E get(int index) {
        synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }
​
    public int indexOf(Object o) {
        synchronized (mutex) {return list.indexOf(o);}
    }
    public int lastIndexOf(Object o) {
        synchronized (mutex) {return list.lastIndexOf(o);}
    }
​
    public boolean addAll(int index, Collection<? extends E> c) {
        synchronized (mutex) {return list.addAll(index, c);}
    }
​
    public ListIterator<E> listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }
​
    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }
​
    public List<E> subList(int fromIndex, int toIndex) {
        synchronized (mutex) {
            return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                          mutex);
        }
    }
​
    @Override
    public void replaceAll(UnaryOperator<E> operator) {
        synchronized (mutex) {list.replaceAll(operator);}
    }
    @Override
    public void sort(Comparator<? super E> c) {
        synchronized (mutex) {list.sort(c);}
    }
​
    /**
         * SynchronizedRandomAccessList instances are serialized as
         * SynchronizedList instances to allow them to be deserialized
         * in pre-1.4 JREs (which do not have SynchronizedRandomAccessList).
         * This method inverts the transformation.  As a beneficial
         * side-effect, it also grafts the RandomAccess marker onto
         * SynchronizedList instances that were serialized in pre-1.4 JREs.
         *
         * Note: Unfortunately, SynchronizedRandomAccessList instances
         * serialized in 1.4.1 and deserialized in 1.4 will become
         * SynchronizedList instances, as this method was missing in 1.4.
         */
    private Object readResolve() {
        return (list instanceof RandomAccess
                ? new SynchronizedRandomAccessList<>(list)
                : this);
    }
}
22.2 CopyOnWriteArrayList

线程安全的ArrayList,操作与ArrayList相同。

加强版的读写锁,写有锁,读无锁。读写不阻塞,优于读写锁。

原理:写入时,会复制一个副本,在副本中写入,写入完成后,替换原来的地址。

特点:性能高,但是消耗额外的空间。

22.3 CopyOnWriteArraySet

线程安全的Set,底层以CopyOnWriteArrayList实现。

实现元素不重复的方式:每次复制副本后,使用addIfAbsent()方法添加元素,先遍历判断是否存在重复,若存在,则扔掉副本。

22.4 ConcurrentHashMap

JDK1.7中使用分段锁。

  • 初始容量默认16段。
  • 不对整个Map加锁,而是对16段分别加锁,即16把锁。
  • 当多线程同时操作同一个段(例如添加元素)时,才会加锁互斥。而操作不同段时,由于是不同的锁,所以不互斥。
  • 最理想的状态,是16个线程同时操作16段,可以不互斥执行。

JDK1.8后使用CAS(比较交换算法)

  • 修改的方法包含三个核心参数,V、E、N
  • V是要更新的变量,E是预期值,N是新值
  • 只有当V等于E时,才将N赋值给V。否则表示被更新过,取消当次操作。

并行和并发:

  • 并行是指同时执行。
  • 并发是指多线程执行,微观串行,宏观并行。(交替执行)

悲观锁乐观锁:

  • 悲观锁是指普通的互斥锁,每次使用无论读写都加锁,性能较低
  • 乐观锁是指在读取时无锁,在写入时,需要先比较版本,相同时才能修改,否则取消

二十三、 Queue

队列,遵循先进先出的原则,即FIFO(First In First Out)

常用方法:

  • add() 添加元素,推荐使用offer()
  • remove() 获得元素并删除,推荐使用poll()
  • element()获得元素不删除,推荐使用keep()
23.1 ConcurrentLinkedQueue

线程安全的Queue,使用CAS算法,高并发下性能最好的queue。

23.2 阻塞队列BlockingQueue接口

添加了两个无限期等待(wait)的方法:

  • put() 添加元素
  • take() 取出并移出元素

用来解决生产消费问题。

23.2.1 实现类ArrayBlockingQueue

使用数组结构实现的有界队列,需要在创建时指定大小。


ArrayBlockingQueue queue = new ArrayBlockingQueue(10);
23.2.2 实现类LinkedBlockingQueue

使用链表结构实现的无界队列,上限为Integer.MAX_VALUE。


LinkedBlockingQueue queue = new LinkedBlockingQueue();
23.2.3 实现生产消费模式

public class Test {
    public static void main(String[] args) {
        // 创建一个只能存储6个元素的仓库
        ArrayBlockingQueue<Car> queue = new ArrayBlockingQueue<Car>(6);
        
        // 创建生产者
        Runnable p = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 15; i++) {
                    Car car = new Car(name + "===" + i);
                    try {
                        queue.put(car);
                        System.out.println("生产了一辆车," + car);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        
        // 创建消费者
        Runnable c = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Car car = queue.take();
                        System.out.println("消费了一辆车," + car);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                }
            }
        };
        
        ExecutorService pool = Executors.newFixedThreadPool(5);
        pool.submit(p);
        pool.submit(p);
        pool.submit(c);
        pool.submit(c);
        pool.submit(c);
        pool.shutdown();
    }
}