死磕java底层(一)—多线程

1.线程和进程

1.1线程和进程的区别

  • 进程 它是内存中的一段独立的空间,能够负责当前应用程序的运行。当前这个进程负责调度当前程序中的全部运行细节(操做系统为进程分配一块独立的运行空间);
  • 线程 它是位于进程中,负责当前进程中的某个具有独立运行资格的空间(进程为线程分配一块独立运行的空间); 进程是负责某个程序的执行,线程是负责进程中某个独立功能的运行,一个进程至少要包含一个线程。
  • 多线程 在一个进程中能够开启多个线程,让多个线程同时去完成某项任务。使用多线程的目的是为了提升程序的执行效率。

1.2线程运行状态

这里写图片描述

经过Thread类或Runnable接口建立线程对象以后进入初始状态;调用start方法进入可运行状态(就绪状态),此时并非真正的运行,只是表明已经作好了运行前的各项装备;若是此线程获取到cpu的时间片,则进入到真正的可运行状态,执行run方法里面的业务逻辑;若是run方法执行完毕或调用stop方法则线程运行结束,进入死亡状态;在运行状态时调用不一样方法也会进入其余不一样状态,若是调用强制运行方法join或休眠方法将进入等待状态,时间到后自动进入就绪状态,随时准备获取cpu时间片;若是看到synchronized则进入同步队列等待状态,或者若是调用了wait方法则进入等待状态,等待状态的线程必需要经过notify唤醒才可进入等待状态,若是其它线程执行完毕,本线程拿到同步锁则进入就绪状态,等待获取cpu时间片。某个线程是否会执行只能看它可否争抢到cpu时间片,可是经过调高优先级来让线程更大几率的被优先执行。 参考文档:https://mp.weixin.qq.com/s?src=11&timestamp=1513562547&ver=581&signature=30FEkCCQvF3E1tt67vYVym5tRNsSk3d8HGe0v9TAonJmhLh4-53fDEBbgwNFOlgp5rAlGFAJQXYnviaFRwiQ9NmbtIWnZGpotGcuV0Ok*3WzWxg4X6e2mxU0JrgbRb&new=1html

2.多线程

多线程运行的原理是:cpu在线程中作时间片的切换。cpu负责程序的执行,在每一个时间点它其实只能运行一个程序而不是多个程序,不停的在多个程序之间高速切换,而一个程序其实就是一个进程即多个线程,说到底其实就是cpu在多个线程之间不停的作高速切换,而开多个线程就是不让cpu歇着,最大程度的压榨它来为程序服务。实现多线程有三种方式:继承Thread类;实现Runnable接口;使用线程池。java

2.1继承Thread类

public class MyExtendsThread extends Thread {
    String flag;

    public MyExtendsThread(String flag){
        this.flag = flag;
    }

    @Override
    public void run(){
        String name = Thread.currentThread().getName();
        System.out.println("线程"+name+"开始工做了...");
        Random random = new Random();
        for (int i = 0;i < 20;i++){
            try {
                Thread.sleep(random.nextInt(10)*100);
                System.out.println(name+"============="+flag);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread t0 = new MyExtendsThread("t0");
        Thread t1 = new MyExtendsThread("t1");

        t0.start();
        t1.start();
//        t0.run();
//        t1.run();
    }
}
复制代码

调用线程要用start方法,而不是run方法,使用run方法只是调用方法,实际执行的仍是Main线程,而调用start方法能够明显的看到线程争抢。git

2.2实现Runnable接口

public class MyThreadImplementRunnable implements Runnable {

    int x;

    public MyThreadImplementRunnable(int x) {
        this.x = x;
    }
    
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println("线程"+name+"开始执行");
        Random random = new Random();
        for(int i = 0;i<20;i++){
            try {
                Thread.sleep(random.nextInt(10)*100);
                System.out.println(name+"============="+x);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThreadImplementRunnable(1),"线程1");
        Thread t2 = new Thread(new MyThreadImplementRunnable(2),"线程2");
    
        t1.start();
        t2.start();
    }
}
复制代码

2.3实现Callable接口

  • 建立实现Callable接口的类MyThreadImplementCallable;
  • 建立一个类对象:MyThreadImplementCallable callable = new MyThreadImplementCallable("测试");
  • 由Callable建立一个FutureTask对象: FutureTask futureTask = new FutureTask(callable); 注意:FutureTask是一个包装器,它经过接受Callable来建立,它同时实现了Future和Runnable接口。
  • 由FutureTask建立一个Thread对象: Thread thread = new Thread(futureTask);
  • 启动线程: thread.start();
  • 获取任务线程执行结果 futureTask.get(); 注意:实现Callable接口的线程能够得到任务线程的执行结果;实现Runnable接口的线程没法获取任务线程执行的结果。
public class MyThreadImplementCallable implements Callable<String> {

    String name;
    public MyThreadImplementCallable(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"开始工做==============");
        Random random  = new Random();
        Thread.sleep(random.nextInt(5)*100);  //模拟执行业务
        return name+":执行完成";
    }

    public static void main(String[] args) throws Exception{
        MyThreadImplementCallable callable = new MyThreadImplementCallable("测试");
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        Thread thread = new Thread(futureTask);

        thread.start();
        String result = futureTask.get();  //获取任务线程执行结果
        System.out.println("线程的执行结果:"+result);
    }
}
复制代码

2.4使用线程池

见下面的线程池专讲。 参考文档:https://www.cnblogs.com/langtianya/archive/2013/03/14/2959713.htmlgithub

3.同步

3.1synchronized关键字

public class MySynchronized {
    public static void main(String[] args){
        final MySynchronized synchronized1 = new MySynchronized();
        final MySynchronized synchronized2 = new MySynchronized();
        new Thread("thread1"){
            @Override
            public void run(){
                synchronized (synchronized1){
                    try {
                        System.out.println(this.getName()+":start");
                        Thread.sleep(1000);
                        System.out.println(this.getName()+":wake up");
                        System.out.println(this.getName()+":end");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread("thread2"){
            @Override
            public void run() {
                synchronized (synchronized1){  //争抢同一把锁时,线程1没释放以前,线程2只能等待
//                synchronized (synchronized2){ //若是不是一把锁,能够看到两句话交叉打印,发生争抢
                    System.out.println(this.getName()+":start");
                    System.out.println(this.getName()+":end");
                }
            }
        }.start();
    }
}
复制代码

synchronized是java中的关键字,属于java语言的内置特性。若是一个代码块使用synchronized修饰,则这块代码是同步的,当一个线程获取到这个锁而且开始执行时,其它线程只能一直眼睁睁的等着这个线程执行而后释放锁,其中释放锁只有两种缘由:1.线程正常执行完毕;2.线程执行时发生异常,jvm自动将锁释放。能够看到使用synchronized关键字以后每一个时刻只会有一个线程执行代码块里面的共享代码,线程安全;缺点也很明显,其它线程只能等锁释放,资源浪费严重。编程

3.2Lock接口

  • lock和synchronized的区别: Lock不是Java语言内置的,它是一个接口,经过这个接口能够实现同步访问,synchronized是Java语言的关键字,是内置特性;Lock和synchronized有一点很是大的不一样,采用synchronized不须要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完以后,系统会自动让线程释放对锁的占用;而Lock则必需要用户去手动释放锁,若是没有主动释放锁,就有可能致使出现死锁现象。 Lock是一个接口,它里面有以下方法:
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
}
复制代码

lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用来获取锁的。 unLock()方法是用来释放锁的。缓存

  • lock就是用来获取锁的,前面说到若是采用Lock,必须主动去释放锁。即便发生异常,程序也不会自动释放锁,所以通常来讲,使用Lock必须在try{}catch{}块中进行,而且将释放锁的操做放在finally块中进行,以保证锁必定被被释放,防止死锁的发生。
public class MyLock {

    private static ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private static Lock lock = new ReentrantLock();

    public static <E> void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();

                lock.lock();  //获取锁
                try {
                    System.out.println(thread.getName() + "获得了锁");
                    for (int i = 0; i < 5; i++) {
                        arrayList.add(i);
                    }
                } catch (Exception e) {
                } finally {
                    System.out.println(thread.getName() + "释放了锁");
                    lock.unlock();  //释放锁
                }

            };
        }.start();

        new Thread() {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                lock.lock();
                try {
                    System.out.println(thread.getName() + "获得了锁");
                    for (int i = 0; i < 5; i++) {
                        arrayList.add(i);
                    }
                } catch (Exception e) {
                } finally {
                    System.out.println(thread.getName() + "释放了锁");
                    lock.unlock();
                }

            };
        }.start();
    }
}
复制代码
  • tryLock()表示用来尝试获取锁,若是获取成功,则返回true,若是获取失败(即锁已被其余线程获取),则返回false,也就说这个方法不管如何都会当即返回,在拿不到锁时不会一直等待。
  • tryLock(long time, TimeUnit unit)方法和tryLock()方法相似,区别在于这个方法在拿不到锁时会等待必定的时间,在时间期限以内若是还拿不到锁,就返回false。若是一开始就拿到锁或者在等待期间内拿到了锁,则返回true。
//观察现象:一个线程得到锁后,另外一个线程取不到锁,不会一直等待
public class MyTryLock {

    private static List<Integer> arrayList = new ArrayList<Integer>();
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread("线程1") {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                boolean tryLock = lock.tryLock();
                System.out.println(thread.getName()+"======="+tryLock);
                if(tryLock){
                    try {
                        System.out.println(thread.getName() + "获得了锁");
                        for(int i = 0;i < 20;i++){
                            arrayList.add(i);
                        }
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                        System.out.println(thread.getName() + "释放了锁");
                    }
                }
            }
        }.start();

        new Thread("线程2") {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                boolean tryLock = lock.tryLock();
                System.out.println(thread.getName()+"======="+tryLock);
                if(tryLock){
                    try {
                        System.out.println(thread.getName() + "获得了锁");
                        for(int i = 0;i < 20;i++){
                            arrayList.add(i);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                        System.out.println(thread.getName() + "释放了锁");
                    }
                }
            }
        }.start();
    }
}
复制代码

线程1和线程2共享成员变量arrayList,当线程1获取锁的时候,线程2就获取不到锁,没办法执行它的业务逻辑,只有等线程1执行完毕,释放了锁,线程2才能获取锁,执行它的代码,进而保证了线程安全。安全

  • lockInterruptibly()方法比较特殊,当经过这个方法去获取锁时,若是线程正在等待获取锁,则这个线程可以响应中断,即中断线程的等待状态。 注意,当一个线程获取了锁以后,是不会被interrupt()方法中断的。 所以当经过lockInterruptibly()方法获取某个锁时,若是不能获取到,只有进行等待的状况下,是能够中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是没法被中断的,只有一直等待下去。
  • Lock接口的实现类——ReentrantLock 直接使用lock接口的话,咱们须要实现不少方法,不方便,ReentrantLock是惟一实现了Lock接口的类,而且ReentrantLock提供了更多的方法,ReentrantLock,意思是“可重入锁”,使用它能够建立Lock对象。
  • ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
复制代码

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操做分开,分红2个锁来分配给线程,从而使得多个线程能够同时进行读操做。bash

  • ReentrantReadWriteLock里面提供了不少丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁,使用这个读写锁操做的结果就是:要么执行的全是读操做,结束完以后全执行写操做,中间不会交叉读写。
/**
 * @author 刘俊重
 * 若是有一个线程已经占用了读锁,则此时其余线程若是要申请写锁,则申请写锁的线程会一直等待释放读锁。
 * 若是有一个线程已经占用了写锁,则此时其余线程若是申请写锁或者读锁,则申请的线程会一直等待释放写锁。
 */
public class MyReentrantReadWriteLock {

    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final MyReentrantReadWriteLock myTest = new MyReentrantReadWriteLock();
        new Thread("线程1"){
            @Override
            public void run(){
                myTest.read(Thread.currentThread());
                myTest.writer(Thread.currentThread());
            }
        }.start();

        new Thread("线程2"){
            @Override
            public void run(){
                myTest.read(Thread.currentThread());
                myTest.writer(Thread.currentThread());
            }
        }.start();
    }

    /**
     * @Description 读方法
     * @Author 刘俊重
     * @Date 2017/12/18
     */
    private void read(Thread thread){
        readWriteLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis()-start<=1){
                System.out.println(thread.getName()+"===正在执行读操做");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
            System.out.println(thread.getName()+"==释放读锁");
        }
    }

    /**
     * @Description 写方法
     * @Author 刘俊重
     * @Date 2017/12/18
     */
    private void writer(Thread thread){
        readWriteLock.writeLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis()-start<=1){
                System.out.println(thread.getName()+"===正在执行写操做");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
            System.out.println(thread.getName()+"==释放写锁");
        }
    }
}
复制代码

Lock和Synchronized的选择:多线程

  • Lock是一个接口,而sysnchrinized是java关键字,属于内置的语言实现;
  • synchronized关键字程序运行完成以后或出现异常时会释放锁,使用lock不会自动释放锁,只能本身使用unlock释放,不然会引发死锁,最好在finally中释放;
  • 使用lock可使用trylock方法判断有没有得到锁,使用synchronized没法判断;
  • 使用lock可让等待锁的线程中断,使用synchronized没法让线程中断,只能一直等待下去;
  • 使用lock能够提升多线程读操做的效率。 结论:若是竞争的资源不激烈,则使用synchronized和lock效率差很少;若是有大量线程同时竞争,则lock要远远优于synchronized。

4.volatile关键字

程序执行时有主内存,每一个线程工做时也有本身的工做内存。当一个线程开始工做时会从主内存中拷贝一个变量的副本到工做内存中,在工做内存中操做完副本时再更新回主内存。当存在多线程时,若是工做内存A处理完还没来得及更新回主内存以前,工做内存B就从主内存中拉取了这个变量,那么很明显这个变量并非最新的数据,会出现问题。怎么解决呢?可使用volatile,volatile有个最显著的特性就是对它所修饰变量具备可见性,什么意思呢,就是当一个线程修改了变量的值,新的值会马上(立刻)同步到主内存中,其它线程使用时拉取到的就是最新的变量值。尽管volatile能保证变量的可见性,但并不能保证线程安全,由于它不能保证原子性。要想线程安全仍是要用同步或者锁。 有一篇文档写volatile写的很好,贴一下:http://dwz.cn/76TMGW并发

5.线程池

JDK1.5以后引入了高级并发特性,在java.util.concurrent包中,是专门用于多线程并发编程的,充分利用了现代计算机多处理器和多核心的功能以编写大规模并发应用程序。主要包含原子量、并发集合、同步器、可重入锁,并对线程池的建立提供了强力的支持。

5.1线程池的5种建立方式

  • Single Thread Executor : 只有一个线程的线程池,全部提交的任务都是顺序执行; 代码: Executors.newSingleThreadExecutor()
  • Cached Thread Pool : 线程池里有不少线程须要同时执行,老的可用线程将被新的任务触发从新执行,若是线程超过60秒内没执行,那么将被终止并从池中删除; 代码:Executors.newCachedThreadPool()
  • Fixed Thread Pool : 拥有固定线程数的线程池,若是没有任务执行,那么线程会一直等待, 代码: Executors.newFixedThreadPool(4) 在构造函数中的参数4是线程池的大小,你能够随意设置,最好设置成和cpu的核数量保持一致,获取cpu的核数量int cpuNums = Runtime.getRuntime().availableProcessors();
  • Scheduled Thread Pool : 用来调度即将执行的任务的线程池,可能不是直接执行, 每隔多久执行一次,属于策略型的。 代码:Executors.newScheduledThreadPool()
  • Single Thread Scheduled Pool : 只有一个线程,用来调度任务在指定时间执行,代码:Executors.newSingleThreadScheduledExecutor() 示例代码以下:
public static void main(String[] args) {
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //获取cpu核心数
        int num = Runtime.getRuntime().availableProcessors();
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(num);
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(8);
        ScheduledExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
    }
复制代码

5.2线程池的使用

说到线程池使用以前再强调一下Runnable的孪生兄弟——Callable,他们两个很像,只是Runnable的run方法不会有任何返回结果,主线程没法得到任务线程的返回值;可是Callable的call方法能够返回结果,可是主线程在获取时是被阻塞,须要等待任务线程返回才能拿到结果,因此Callable比Runnable更强大,那么怎么获取到这个执行结果呢?答案是Future,使用Future能够获取到Callable执行的结果。 如今开始说线程池怎么使用,也有两种方式,一种Runnable的,一种Callable的:

  • 提交 Runnable ,任务完成后 Future 对象返回 null,调用excute,提交任务, 匿名Runable重写run方法, run方法里是业务逻辑。 示例代码:
public class TestPoolWithRunnable {
    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for (int i=0;i<10;i++){
            Future<?> submit = pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                }
            });
            System.out.println("执行结果:"+submit.get());  //全部的执行结果全是null
        }
        pool.shutdown();  //关闭线程池
    }
}
复制代码
  • 提交 Callable,该方法返回一个 Future 实例表示任务的状态,调用submit提交任务, 匿名Callable,重写call方法, 有返回值, 获取返回值会阻塞,一直要等到线程任务返回结果。
/**
 * @author 刘俊重
 * Callable 跟Runnable的区别:
 * Runnable的run方法不会有任何返回结果,因此主线程没法得到任务线程的返回值
 * Callable的call方法能够返回结果,可是主线程在获取时是被阻塞,须要等待任务线程返回才能拿到结果
 */
public class TestPoolWithCallable {

    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for(int i=0;i<10;i++){
            Future<String> future = pool.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(500);
                    return "===="+Thread.currentThread().getName();
                }
            });
            //从Future中get结果,这个方法是会被阻塞的,一直要等到线程任务返回结果
            System.out.println("执行结果:"+future.get());
        }

        pool.shutdown();
    }
}
复制代码

如何解决获取执行结果阻塞的问题? 在使用future.get()方法获取结果时,这个方法是阻塞的,怎么提升效率呢?若是在不要求立马拿到执行结果的状况下,能够先将执行结果放在队列里面,待程序执行完毕以后在获取每一个线程的执行结果,示例代码以下:

public class TestThreadPool {

    public static void main(String[] args) throws Exception{
        Future<?> submit = null;
        //建立缓存线程池
        ExecutorService cachePool = Executors.newCachedThreadPool();

        //用来存在Callable执行结果
        List<Future<?>> futureList = new ArrayList<Future<?>>();

        for(int i = 0;i<10;i++){
            //cachePool提交线程,Callable,Runnable无返回值
            //submit = cachePool.submit(new TaskCallable(i));
            submit = cachePool.submit(new TaskRunnable(i));

            //把这些执行结果放到list中,后面再取能够避免阻塞
            futureList.add(submit);
        }
        cachePool.shutdown();
        //打印执行结果
        for(Future f : futureList){
            boolean done = f.isDone();
            System.out.println(done?"已完成":"未完成");
            System.out.println("线程返回结果:"+f.get());
        }
    }
}
复制代码

把submit放在list集合中,线程直线完毕以后再取。

6.java并发编程总结

6.1不使用线程池的缺点

直接使用new Thread().start()的方式,对于通常场景是没问题的,但若是是在并发请求很高的状况下,就会有隐患:

  • 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来讲,新建一个线程的代价仍是很大的,决不一样于新建一个对象。
  • 资源消耗量。没有一个池来限制线程的数量,会致使线程的数量直接取决于应用的并发量。
  • 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题。

6.2线程池的类型

不论是经过Executors建立线程池,仍是经过Spring来管理,都得知道有哪几种线程池:

  • FixedThreadPool:定长线程池,提交任务时建立线程,直到池的最大容量,若是有线程非预期结束,会补充新线程;
  • CachedThreadPool:可变线程池,它犹如一个弹簧,若是没有任务需求时,它回收空闲线程,若是需求增长,则按需增长线程,不对池的大小作限制;
  • SingleThreadExecutor:单线程。处理不过来的任务会进入FIFO队列等待执行;
  • SecheduledThreadPool:周期性线程池。支持执行周期性线程任务 其实,这些不一样类型的线程池都是经过构建一个ThreadPoolExecutor来完成的,所不一样的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory这么几个参数。

6.3线程池饱和策略

由以上线程池类型可知,除了CachedThreadPool其余线程池都有饱和的可能,当饱和之后就须要相应的策略处理请求线程的任务,好比,达到上限时经过ThreadPoolExecutor.setRejectedExecutionHandler方法设置一个拒绝任务的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy几种策略。

我的博客地址:http://catchu.github.io

相关文章
相关标签/搜索