一、进程与线程

1.1 定义

进程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

1.2 区别

  1. 进程是资源分配最小单位,线程是程序执行的最小单位;
  2. 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;
  3. CPU创建/切换一个线程比切换进程花费小;
  4. 线程占用的资源要比进程少很多。
  5. 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
  6. 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);

1.3 打比方

  1. 加强理解,做个简单的比喻:进程=火车,线程=车厢
  2. 线程在进程下行进(单纯的车厢无法运行)
  3. 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  4. 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  5. 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  6. 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
    7.进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  7. 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  8. 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
  9. 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

二、多线程

2.1 创建线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 应用程序可以使用Executor框架来创建线程池

2.2 线程状态

image.png

  1. 新建( new ):新创建了一个线程对象;
  2. 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取CPU的使用权;
  3. 运行( running ):可运行状态( runnable )的线程获得了CPU时间片( timeslice ) ,执行程序代码;
  4. 阻塞( block ):阻塞状态是指线程因为某种原因放弃了CPU 使用权,也即让出了 CPU timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。
    1. 等待阻塞:运行( running )的线程执行 o.wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。
    2. 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
    3. 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
  5. 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。

2.3 同步方法和同步代码块

区别

  1. 同步方法默认用this或者当前类class对象作为锁;
  2. 同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;

2.4 监视器(Monitor)

监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。
每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。

2.5 死锁(deadlock)

两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。

产生条件

只要破坏其中任意一个条件,就可以避免死锁

  1. 互斥:一个资源每次只能被一个进程使用。
  2. 等待:一个进程因请求资源而阻塞时,对已获得资源保持不放。
  3. 不可抢占:进程已获得资源,在未使用完成前,不能被剥夺。
  4. 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

2.6 数据竞争

package bardese.com.wecom.u;

public class AccumulateWrong {

    private static int count = 0;

    public static void main(String[] args) throws Exception {
        Runnable task = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000000; ++i) {
//                    我们在执行count += 1;这行代码时,实际在CPU上运行的会是多条指令:
//                        1. 获取count变量的当前值
//                        2. 计算count + 1的值
//                        3. 将count + 1的结果值存到count变量中
//                    这种在多个线程中对共享数据进行竞争性访问的情况就被称为数据竞争,可以理解为对共享数据的并发访问会导致问题的情况就是数据竞争。
                    count += 1;
                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}


2.7 synchronized

package bardese.com.wecom.u;

public class AccumulateSynchronized {

    private static int count = 0;

    public static void main(String[] args) throws Exception {
        Runnable task = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000000; ++i) {
                    //synchronized发挥的作用就是让两个线程互斥地执行count += 1;语句。
                    // 所谓互斥也就是同一时间只能有一个线程执行,如果另一个线程同时也要执行的话则必须等到前一个线程完成操作退出synchronized语句块之后才能进入。
                    synchronized (this) {
                        count += 1;
                    }
                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}


2.8 ReentrantLock

package bardese.com.wecom.u;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class AccumulateReentrantLock {

    private static int count = 0;

    public static void main(String[] args) throws Exception {
        final ReentrantLock lock = new ReentrantLock();
        Runnable task = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000000; ++i) {
                    lock.lock();
                    try {
                        count += 1;
                    } finally {
                        lock.unlock();
                    }
                    //ReentrantLock可以实现带有超时时间的锁等待,我们可以通过tryLock方法进行加锁,并传入超时时间参数。
                    // 如果超过了超时时间还么有获得锁的话,那么就tryLock方法就会返回false;
//                    try {
//                        boolean tryLock = lock.tryLock(1, TimeUnit.SECONDS);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }


                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}


2.9 Atomic

package bardese.com.wecom.u;

import java.util.concurrent.atomic.AtomicInteger;

public class AccumulateAtomic {
    //AtomicInteger提供了原子性的变量值修改方式,原子性保证了整个累加操作可以被看成是一个操作,不会出现更细粒度的操作之间互相穿插导致错误结果的情况。
    // 在底层AtomicInteger是基于硬件的CAS原语来实现的,CAS是“Compare and Swap”的缩写,意思是在修改一个变量时会同时指定新值和旧值,只有在旧值等于变量的当前值时,才会把变量的值修改为新值。
    // CAS操作在硬件层面是可以保证原子性的。
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        Runnable task = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000000; ++i) {
                    count.incrementAndGet();
                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}


2.10 内存可见性

package bardese.com.wecom.u;

public class MemoryVisibilityDemo {
    // 较完整的计算机存储体系从下到上依次有外存、内存、“L3、L2、L1三层高速缓存”、寄存器这几层。
    // 在这个存储体系中从下到上是一个速度从慢到快的结构,越上层速度越快,所以当CPU操作内存数据时会尽量把数据读取到内存之上的高速缓存中再进行读写。
    // 所以如果程序想要修改一个变量的值,那么系统会先把新值写到L1缓存中,之后在合适的时间才会将缓存中的数据写回内存当中。
    // 但L1、L2两级高速缓存是核内缓存,如果我们在一个核中运行的线程上修改了变量的值而没有写回内存的话,其他核心上运行的线程就看不到这个变量的最新值了。
    private static boolean flag;

    public static void main(String[] args) throws Exception {

        for (int i = 0; i < 10000; ++i) {
            flag = false;
            final int no = i;

            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    flag = true;
                    System.out.println(String.format("No.%d loop, t1 is done.", no));
                }
            });

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!flag) {
                        // 因为修改和读取静态变量flag的代码在两个不同的线程中,所以在多核处理器上运行这段程序时,就有可能在两个不同的处理器核心上运行这两段代码。
                        // 最终就会导致线程t1虽然已经把flag变量的值修改为true了,但是因为这个值还没有写回内存,所以线程t2看到的flag变量的值仍然是false,程序卡死。
                    }

                    System.out.println(String.format("No.%d loop, t2 is done.", no));
                }
            });

            t2.start();
            t1.start();

            t1.join();
            t2.join();
        }
    }

}

package bardese.com.wecom.u;

public class MemoryVisibilityVolatile {
    //volatile变量保证了对该变量的写入操作和在其之后的读取操作之间存在同步关系,这个同步关系保证了对volatile变量的读取一定可以获取到该变量的最新值。
    // 在底层,对volatile变量的写入会触发高速缓存强制写回内存,该操作会使其他处理器核心中的同一个数据块无效化,必须从内存中重新读取。

    private static volatile boolean flag;

    public static void main(String[] args) throws Exception {

        for (int i = 0; i < 10000; ++i) {
            flag = false;
            final int no = i;

            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    flag = true;
                    System.out.println(String.format("No.%d loop, t1 is done.", no));
                }
            });

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!flag) {
                    }

                    System.out.println(String.format("No.%d loop, t2 is done.", no));
                }
            });

            t2.start();
            t1.start();

            t1.join();
            t2.join();
        }
    }

}

三、线程的通信与停止

3.1 四种通信方式

同步

多个线程通过synchronized关键字这种方式来实现线程间的通信。这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

while轮询的方式
  1. 线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件是否成立,从而实现了线程间的通信。
  2. 这种方式会浪费CPU资源,是因为JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试 某个条件是否成立。
wait/notify机制
  1. 线程A要等待某个条件满足时(a==10)才执行操作。线程B运行时(a++)会修改变量触发条件达成。
  2. 当条件未满足时线程A调用wait() 放弃CPU,并进入阻塞状态。当条件满足时,线程B调用 notify()通知 线程A,唤醒线程A并让它进入可运行状态。
  3. 这种方式的一个好处就是CPU的利用率提高了,
管道通信

使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信。

3.2 停止线程运行

三种线程停止方式

停止一个线程意味着在任务处理完任务之前停掉正在做的操作,也就是放弃当前的操作。

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
  3. 使用interrupt方法中断线程。interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。
判断线程是否停止
  1. thread.interrupted():测试当前线程是否已经中断并清除中断状态,当前线程是指运行this.interrupted()方法的线程,连续两次调用该方法,则第二次调用返回false。
  2. thread.isInterrupted: 判断thread线程是否已经中断,不清除中断状态。

四、创建线程的方式

4.1 继承Thread类

public class FirstThreadTest extends Thread{
    int i = 0;
    //重写run方法,run方法的方法体就是现场执行体
    public void run()
    {
        for(;i<10;i++){
			//this.getName()
			//getName()
            System.out.println(Thread.currentThread().getName()+"  "+i);
        }
    }
    public static void main(String[] args)
    {
        for(int i = 0;i< 10;i++)
        {
            System.out.println(Thread.currentThread().getName()+"  : "+i);
            if(i==2)
            {
                new FirstThreadTest().start();
                new FirstThreadTest().start();
            }
        }
    }
}
  1. 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
  2. 线程类已经继承了Thread类,所以不能再继承其他父类。

4.2 实现Runnable接口

public class RunnableThreadTest implements Runnable  
{  

    private int i;  
    public void run()  
    {  
        for(i = 0;i <100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
    }  
    public static void main(String[] args)  
    {  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
            if(i==20)  
            {  
                RunnableThreadTest rtt = new RunnableThreadTest();  
                new Thread(rtt,"新线程1").start();  
                new Thread(rtt,"新线程2").start();  
            }  
        }  
    }   
}

4.3 通过Callable和Future创建线程

  1. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  2. FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
import java.util.concurrent.Callable;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.FutureTask;  

public class CallableThreadTest implements Callable<Integer>  
{  

    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  

    }  

    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

4.4 对比

  1. 采用实现Runnable、Callable接口的方式创建多线程时,线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  2. Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
  3. Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  4. call方法可以抛出异常,run方法不可以。
  5. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

五、JUC框架图

image.png

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议