最近几个月终于有大把时间总结这两年来所学 2019.5.29

前言

java中的多线程包括下面两点

  1. 多线程怎么用
  2. 线程安全

区分几个概念

区别一下 进程、线程、CPU线程、操作系统线程

  • 进程:操作系统中一块独立的区域,和操作系统独立开,数据不共享,相互隔离。
  • 线程:工作在进程中的工作单元,可以共享资源。
  • CPU线程:CPU在硬件级别同时能做的事情(注意是硬件层面,而非软件上做的时间切片)。有做过单片机的裸机代码的同学,对这个概念应该会非常熟悉。
  • 操作系统线程:模仿的CPU线程,把CPU的线程做进一步拆分,分为一个个的时间切片,轮询的做各种不同的工作
  • 区别线程和进程,本质上来说,线程(thread)和进程(Process)不是一个概念,进程本身是一个运行的程序,而线程是程序中的工作单元。Thread 的英文本意是棉毛线的意思,而Process是过程,工序的意思。对比一下两者的英文意思,可以很好地理解两个概念。
  • 线程池的大小和CPU的核心数。一般线程池的大小和CPU的核心数要成正相关

java中实现多线程

简要列举一下java中常用的多线程的实现。

  1. Thread 创建一个thread,重写run方法。然后thread.start();
static void thread() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("use thread");
            }
        };
        thread.start();
    }
复制代码
  1. runnable
static void runnable() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("use runnable");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
复制代码

Runnable 和 Thread两个方法关联是很紧密的,我最近看了看源码,发现,Thread里面的run方法是长这个样子的

public void run() {
        if (this.target != null) {
            this.target.run();
        }
    }
复制代码

然后我们看到里面有一个target,这个target就是 Thread thread = new Thread(runnable);里面的runnable。 所以对于Thread和Runnable两个方法来说,我们如果重写了Runnable,就会运行runnable的run方法,如果写了Thread的run方法,就会运行Thread的run方法。如果两个都写了,两个都会运行。 3. ThreadFactory

static void threadFactory() {
        ThreadFactory factory = new ThreadFactory() {
            AtomicInteger count = new AtomicInteger(0);
//            int count = 0;

            @Override
            public Thread newThread(Runnable r) {
//                count++;
                return new Thread(r, "Thread-" + count.incrementAndGet());
            }
        };

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " started!");
            }
        };

        Thread thread = factory.newThread(runnable);
        thread.start();
        Thread thread1 = factory.newThread(runnable);
        thread1.start();
    }
复制代码

这种方法用的比较少,简单看看就可以。

  1. Executor 线程池 这是非常非常非常常用的方法。有四种常用的方法
  • 基本用法
static void executor() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread with Runnable started!");
            }
        };
        Executor executor = Executors.newCachedThreadPool();
        executor.execute(runnable);
    }

复制代码
  • 基本构造方法
ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)
变量含义分别为:
核心线程数;
最大线程数;
keep alive 的时间;单位;
装runnable的队列
复制代码

几个变量的判断优先级为:corePoolSize > workQueue > maximumPoolSize; 解释一下: 当前线程数 < corePoolSize,新任务来临,无论是否有空的线程任务,都会优先创建核心线程; corePoolSize < 当前线程数 < maximumPoolSize,只有当workQueue满了,才会创建新的线程去做任务,否则都将默认排队执行;

  • 下面就这个构造基本的构造方法来介绍一下我们常用的四中线程池。

a.newCachedThreadPool

new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
//核心线程数:0;最大线程数:无穷大;等待时间:60S
复制代码

从名字也可以看出,这是可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,如果没有空线程等待回收,则新建线程。

b.newSingleThreadPool :

new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心线程数:1;最大线程数1;永不回收
复制代码

特点:线程一直存在,随时可以拿来用。可以用来做取消功能。

c.newFixedThreadPool

new ThreadPoolExecutor(32, 32, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心线程数:自定义;最大线程数:核心线程数;不回收
复制代码

做集中的事情,比如下载文件,诉求就是,我需要马上用很多线程,而且就用这么一下,用完了我就不要了。

d.newSchedualThreadPool:

new ThreadPoolExecutor(32, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue());

复制代码

可以设置定时,以及周期性执行任务。

  1. callable:一个有返回值的runnable 后台任务在后台执行,返回值返回给谁?和Future类配合使用,将返回的值给future的get方法。如下面的程序段所示 注意,Future.get是一个阻塞方法,会一直等到callable执行完成,才会返回数据。
static void callable() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "Done";
            }
        };

        ExecutorService executor = Executors.newCachedThreadPool();
        Future<String> future = executor.submit(callable);
        try {
            String result = future.get();
            System.out.println("result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

复制代码

synchronized

  • 原子操作:CPU级别,只执行一次的代码。不可被拆分
  1. synchronized的互斥 为了保护方法或者代码块内的数据,重点是保护数据。我们关注的不是方法,而是资源,只有处理同样数据方法,才需要做互斥操作 每一个synchronized都有对应一个monitor, 只有相同的monitor才具有互斥性。 非静态方法的monitor是类的实例化对象。静态方法的monitor是类。
public static  synchronized void methodA(){
        //do something
    } 
    
    public synchronized void methodB(){
        //do something
    }

    private final Object monitorC = new Object();
    public void methodC(){
        synchronized (monitorC){
            //do something
        }
    }
复制代码

看上面的一段代码,methodA/methodB/methodC三个方法,都加了synchronized,但是他们的monitor分别是:className.class,this,monitorC,所以三个方法可以同时被调用。我们的锁,是针对每个monitor进行锁的,两个方法在同一个monitor下,可以达到锁的作用,如果不在一个monitor下,就达不到锁的作用。同时,对于不是操作同样数据的方法,是没有必要加互斥锁的

  1. 死锁
  • 两个锁一起使用的时候,会出现死锁。比如下面这中情况
public class DeadLock {
    private final Object monitor1 = new Object();
    private final Object monitor2 = new Object();
    
    private void setName(int name){
        synchronized (monitor1){
            name = 1;
            //线程1运行到此处的时候,被线程2打断,跑到setAge中
            synchronized (monitor2){
                name = 2;
            }
        }
    }
    private void setAge(int age){
        synchronized (monitor2){
            age = 1;
            //线程是运行到此处,就会出现死锁
            synchronized (monitor1){
                age = 2;
            }
        }
    }
}

复制代码

上面的代码中,注释里面有详细说明。monitor1和monitor2在相互等待对方运行完成,导致死锁产生。

  1. 乐观锁、悲观锁 关于数据库读写的问题。比如账户余额,用于存入100块,需要先读出原来的金额,然后+100。但是该操作可能会同时进行,比如两个人同时给你转账。 乐观锁:写入数据前,先核对一下数据有没有变化,如果有变化,那么就加一个锁。 悲观锁:无论有没有人写,都先把把锁加上。

volatile

  • 相当于一个轻量级的synchronized。保证修饰的变量具有同步性,即被修饰的变量被赋值时,具有原子性。无法解决++的问题。

其他

  1. 解决++的非原子问题:使用Atomic

看看上面的threadFactory的示例代码中。

  1. 读写锁 上面有说道,同步方法本质是为了保护方法中处理的数据。所以我们在读写方法中,使用读写锁来解决synchronized过重的问题
public class ReadWriteLockDemo {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private int x = 0;

    private void write() {
        writeLock.lock();
        try {
            x++;
        } finally {
            writeLock.unlock();
        }
    }

    private void read(int time) {
        readLock.lock();
        try {
            for (int i = 0; i < time; i++) {
                System.out.print(x + " ");
            }
            System.out.println();
        } finally {
            readLock.unlock();
        }
    }
}
复制代码
  • 以上为线程的相关简要整理。