Java线程基础

218 阅读5分钟

线程总览

  • Java多线程和线程同步
  • Android中线程间通信的本质和原理:Handler和Ascytask,
  • RxJava使用及原理
  • 线程与kotlin中的协程

CPU缓存和指令重排

会造成多线程的结果不可控,即线程结果不同步

Java内存模型

Java Memroy Model即JMM 本质是一套规范happens-before,即先行发生的结果对后续是可见的

volatile和synchronized

volatile和synchronized等关键字都是实行happens-before化

线程基础

什么是线程

操作系统在运行一个程序的时候,会为其创造一个进程。例如,启动一个Java程序,操作系统就会为其创建一个Java进程。操作系统调度的最小单元就叫线程。在一个进程中可以创建多个线程,这些线程拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

  • 一个Java程序的运行,就包括了main线程和其他线程的同时运行
  • 线程安全指的是一个类可以被多线程正确访问

线程的状态

一个线程被创建,只会start一次

  • New 表示新建线程
  • Runnable 运行中的线程,run中的方法
  • Blocked 运行中的线程,被阻塞挂起
  • Waiting 运行中的线程,处于等待状态中
  • Timed Waiting 运行中的线程,调用了sleep正在计时等待
  • Terminated 线程终止,可能时正常退出,也可能时异常终止

为什么要使用多线程

单个线程一个时刻只能运行在一个处理器核心上,而面对现在强大的多核处理器,为了充分“榨取”处理器资源,提高程序响应速度,所以要使用多线程,同时也可以将一些不太重要的业务逻辑放到其他线程。

什么是守护线程(Daemon thread)

JVM启动线程,线程都终止则JVM退出,当线程终止不了呢?那就需要守护线程来终止 通过thread.setDaemon(true)来开启守护线程

线程基本操作

如何开启一个线程

  • 1)通过Runable接口实现
  private static void thread() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("thread start");
            }
        };
        thread.start();
    }

或者

 private static void run() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("thread by runnable");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }

两种方式没有本质区别

  • 2)通过实现callable接口,这一方式相对其他方式,有返回值
    private static void callable() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() {
                int a = 1+2;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return a + "";
            }
        };
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<String> result = executorService.submit(callable);
        try {
            String resultStr = result.get();
            System.out.println(resultStr);
            executorService.shutdown();
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
    }

也可以通过Executors直接开启线程,后面回讲到

join

在线程B中调用线程A.join(),指的是线程B等待线程A终止后,再执行后续任务,join()可以指定时间,比如10ms后不继续等待,直接执行线程B的任务

public static void main(String[] args) throws InterruptedException {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread1 running");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    thread1.join(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread2 running");
            }
        });
        System.out.println("start");
        thread1.start();
        thread2.start();
        System.out.println("end");
    }

执行结果为

start
end
thread2 running
thread1 running

yield

把自己的时间片暂时让出一段时间,不是等结束,而是下一个时间继续

死锁

两个线程持有不同的锁,都想获取彼此手中的锁,就会造成死锁。死锁的解决办法是一定要获取锁的顺序

wait/notify

  • 在synchronized内部,wait用来使线程进入等待状态,此时会释放锁
  • 在synchronized内部,notify/notifyall 用来唤醒线程

ReentrantLock

synchronized锁比较重,ReentrantLock可以自己控制锁的粒度,与Condition搭配可以实现Synchronized+wait+notify的操作

  • await 释放锁并进入等待状态,唤醒线程从await返回后,需要重新获取锁
  • singal 唤醒某个线程
  • singalAll会唤醒全部线程
  private static class TaskQueue {
        private Queue<Integer> queue = new LinkedList<>();
        private  ReentrantLock lock = new ReentrantLock();
        private Condition mCondition;

        public TaskQueue() {
            mCondition = lock.newCondition();
        }

        public void addTask(int value) {
            lock.lock();
            try {
                queue.add(value);
                mCondition.signalAll();
            }finally {
                lock.unlock();
            }
        }

        public int getTask() throws InterruptedException {
            lock.lock();
            try {
                while (queue.isEmpty()) {
                    mCondition.await();
                }
                return queue.remove();
            }finally {
                lock.unlock();
            }

        }

读写锁

通常多线程写数据的话容易造成数据不一致问题,而读操作不会,所以通过读写锁可以区分写少读多的情况

  • ReentrantReadWriteLock只允许一个线程写入
  • ReentrantReadWriteLock允许多个线程在没有写入情况下读取
  • ReentrantReadWriteLock适合读多写少的情况
private int[] ints = new int[10];
    private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock mReadLock;
    private final ReentrantReadWriteLock.WriteLock mWriteLock;

    public ReadWriteLockThread() {
        mReadLock = mLock.readLock();
        mWriteLock = mLock.writeLock();
    }

    public void write(int index) {
        mWriteLock.lock();
        try {
            ints[index] += 1;
        } finally {
            mWriteLock.unlock();
        }
    }

    public int[] read() {
        mReadLock.lock();
        try {
            return Arrays.copyOf(ints, ints.length);
        } finally {
            mReadLock.unlock();
        }
    }

Atomic

Atomic用于基本类型数据的原子性读写,

  • 原子操作通过CAS(compare and set 重点关注unsafe类)实现了无锁的线程安全
  • 适用于累加器、计数器、id唯一生成等

javap 命令

javap 命令主要用来反编译java的class文件,查看java编译器生成的字节码 通过javap命令包含如下:

C:\projects\AndroidLearn\hotfix>javap
用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

通过javap -c xxx.class,

线程池

《阿里巴巴Java开发手册》

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:

  • 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
  • 2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

利用三个线程打印依次打印1A,2B,3C,1D,2E...

private static class Run implements Runnable{


        @Override
        public void run() {
            synchronized (PrintThread.class) {
                int threadNo = Integer.valueOf(Thread.currentThread().getName());
                while (index < 26) {
                    if (index % 3 == threadNo -1) {
                        System.out.println(threadNo + " " +  (char)(aChar++));
                        index++;
                        PrintThread.class.notifyAll();
                    }else {
                        try {
                            PrintThread.class.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

        }
    }

参考