Java多线程学习笔记

Java多线程

启动多线程的方法们

继承Thread类

重写该类的run()方法.然后创建该类的实例,调用start()方法来启动该线程.
Thread.currentThread() 是Thread类的静态方法,返回当前正在执行的线程对象.
实例代码如下:

public class FirstThread extends Thread {
    public void run() {
        for (int 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) {
                new FirstThread().start();
                new FirstThread().start();
            }
        }
    }
}

实现Runnable接口创建线程类

实现Runnable接口,并重写该接口的run()方法,创建Runnable接口实现类的实例,并以此作为Thread的target来创建Thread对象,并start();
示例代码如下:

public class SecondThread implements Runnable{
    public void run() {
        for (int 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) {
                SecondThread secondThread = new SecondThread();
                new Thread(secondThread).start();
                //还可以给线程起名字
                new Thread(secondThread, "我的线程").start();
                //也可以直接用匿名内部类来写
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("i love you");
                    }
                }).start();
                //使用lambda表达式来写
                new Thread(() -> System.out.println("Hello"));
            }
        }
    }
}

实现Callable接口

创建并启动有返回值的线程的步骤如下:

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable()实现类的实例.
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值.
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程.
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值.
    示例代码:
public class ThirdThread {
    public static void main(String[] args) {
        //创建Callable对象
        ThirdThread rt = new ThirdThread();
        //先使用lambda表达式创建Callable<Integer>对象
        //使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            //call()方法可以有返回值
            return i;
        });
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 20) {
                //实质还是以Callable对象来创建并启动线程
                new Thread(task, "有返回值的线程").start();
            }
        }
        try {
            //能够获取到线程的返回值
            System.out.println("子线程的返回值" + task.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三种方式的对比

推荐使用实现Callable和Runnable接口的方式来写多线程.

线程的生命周期

新建和就绪状态

运行和阻塞状态(这里的阻塞状态就是最下面的两种waitting状态)

Thread.sleep()可以让线程休眠
线程进入阻塞状态的几种情况:

  1. 调用sleep()方法,主动放弃所占用的处理器资源
  2. 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞.
  3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程持有.
  4. 线程在等待某个通知.
  5. 程序调用了suspend()方法..

以下情况重新回到就绪状态:

  1. sleep()睡够了.
  2. 线程调用的阻塞式IO方法已返回.
  3. 线程获得了试图获得的同步监视器.
  4. 线程获得了其他线程的通知.
  5. 处于挂起状态的线程被调用了resume()方法.

线程死亡

线程的状态转换图

控制线程

join线程

当某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程执行完为止.
示例代码:

public class JoinThread extends Thread{
    //提供一个有参数的构造器,用于设置该线程的名字
    public JoinThread(String name) {
        super(name);
    }
    //重写run方法,定义线程执行体
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + " " + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //启动子线程
        new JoinThread("新线程").start();
        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                JoinThread jt = new JoinThread("被Join的线程");
                jt.start();
                //main线程调用了jt线程的join()方法,所以他必须等待jt执行结束后才会向下执行.
                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

在该程序里,一共有3个线程,主方法开始时就启动了名为"新线程"的子线程,该子线程将和main线程并发执行,当主线程的循环变量i等于20时,启动了名为"被join线程"的线程,该线程不会和main线程并发执行,main线程必须等该线程执行完毕才可以向下执行.所以只有第一个"新线程" 和"被join的线程" 两个线程并发执行.

后台线程

又叫做守护线程,如果所有前台线程死亡,后台线程会自动死亡.
调用Thread对象的,setDaemon(true) 方法可将指定线程设置为后台线程.

线程睡眠 sleep

Thread.sleep()

线程让步

只是谦让一下而已 Thread.yield()

改变线程优先等级

Thread.setPriority(int newPriority)
newPriority 有三个静态常量 Thread.MAX_PRIORITY Thread.MIN_PRIORITY Thread.NORM_PRIORITY
高优先级的线程将更容易获得执行机会.

线程同步

同步代码块

synchronized(obj) {
    ...
    //此处的代码就是同步代码块
}

线程开始执行同步代码块之前,必须先获得同步监视器的锁定.
任何对象都可以作为同步监视器.

同步方法

同步方法就是使用synchronized关键字来修饰某个方法.该方法称为同步方法.这种方法无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象.

释放同步监视器的锁定

  1. 同步方法,同步代码块执行结束.
  2. 在同步方法,同步代码块里遇到了break,,return终止了该代码块.
  3. 出现了异常或错误.
  4. 程序执行了同步监视器对象的wait()方法.则当前线程暂停,并释放同步监视器.

以下情况,线程并不会释放同步监视器:

  1. 程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程执行.
  2. 其他线程调用了该线程的suspend()方法将该线程挂起.该线程不会释放同步监视器.

同步锁

比较常用的锁是ReentrantLock,使用方法如下:

class X {
    //定义锁对象
    private final ReentrantLock lock = ReentrantLock();
    //定义需要保证线程安全的方法
    public void m() {
        //加锁
        lock.lock();
        try {
            //需要保证线程安全的代码
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

可以多次加锁,但是也要相应次数的解锁.

死锁

互相等待对方释放资源.

线程通信

传统的线程通信.

同步方法可以直接调用 wait() notify() notifyALl()方法.
同步代码块用同步监视器来调用这三个方法.
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程.
notify():唤醒在此同步监视器上等待的单个线程,如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程.只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程.
notifyAll():唤醒在此监视器上等待的所有线程.只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程.

使用condition控制线程通信.

如果程序使用的锁,那么就需要使用condition来控制通信.
Condition实例被绑定在一个lock对象上,要获得特定lock实例的Condition实例,调用lock对象的newCondition()方法即可.Condition类提供了如下三个方法.
await(), singal(), singalAll().基本上差不多,你就new完lock之后,再添加个lock.newConditon()获得condition对象即可.由condition对象来wait,来notify

使用阻塞队列来控制线程通信.

当生产者试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞.当消费者线程试图从BLockingQueue中取出元素时,如果该队列已空,则该线程被阻塞.
BlockingQueue有两个支持阻塞的方法:
put(E e) 把元素放入队列
take() 从队列取出元素.

线程组和未处理的异常

线程可以在新建的时候传一个线程组参数进去,来分配线程的线程组.具体不多说,用的不多.

线程池

Executors工厂类来生产线程池.该工厂类包含如下几个静态工厂方法来创建线程池.

  1. ExecutorService newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程.这些线程将会被缓存到线程池中.
  2. ExecutorService newFixedThreadPool(int nThreads):创建一个可重用的, 具有固定线程数的线程池.
  3. ExecutorService newSingleThreadExecutor(): 创建一个只有单线程的线程池,它相当于调用 newFixedThreadPool方法是传入参数1
  4. newScheduledThreadPool(int corePoolSize):创建一个具有指定线程数的线程池,它可以在指定延迟后执行线程任务.corePoolSize指池中所保存的线程数,即是线程是空闲的也被保存在线程池内.
  5. newSingleThreadScheduledExecutor():上面方法的参数1版本.
  6. ExecutorService newWorkStealingPool(int parallelism): 创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争.
    ExecutorService 代表尽快执行线程的线程池(只要有空闲线程,就会立刻执行线程任务),程序只要将一个Callable或者Runnable对象提交给线程池,就会尽快被执行.还有一个shutdown方法来关闭线程池.
    示例代码:
public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(6);
        Runnable target = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        };
        //向线程池中提交两个线程
        pool.submit(target);
        pool.submit(target);
        //关闭线程池
        pool.shutdown();
    }
}

ForkJoinPool

可以将一个任务拆分为多个小任务,最后再将结果合并起来.
ForkJoinPool 通过以下两个静态方法提供通用池功能.
ForkJoinPool commonPool()该方法返回一个通用池.
int getCommonPoolParallelism() 该方法返回通用池的并行级别.
创建了ForkJoinPool实例之后,就可调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务了.ForkJoinTask 代表一个可以并行、合并的任务,他是一个抽象类,他还有两个抽象子类,RecuriveAction 和 RecuriveTask ,后者代表有返回值的任务,前者是没有返回值的任务.
例如没有返回值的大任务,打印0~300:

public class PrintTask extends RecursiveAction {

    //每个小任务最多只打印50个数.
    private static final int THRESHOLD = 50;
    private int start;
    private int end;

    //打印从start到end的任务
    public PrintTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        //当end与start之间的差距小于THRESHOLD时,开始打印
        if (end - start < THRESHOLD) {
            for (int i = start; i < end; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        } else {
            //将大任务分解成两个小任务.
            int middle = (start + end) / 2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            //并行执行两个小任务
            left.fork();
            right.fork();
        }
    }

    public static void main(String[] args) throws Exception {
        ForkJoinPool pool = new ForkJoinPool();
        pool.submit(new PrintTask(0, 300));
        pool.awaitTermination(2, TimeUnit.SECONDS);
        //关闭线程池
        pool.shutdown();
    }
}

有返回值的任务(计算1~200的累加值):

//继承RecursiveTask来实现可分解的任务
public class CalTask extends RecursiveTask<Integer> {

    //每个小任务最多只累加20个数
    private static final int THRESHOLD = 20;
    private int arr[];
    private int start;
    private int end;

    // 累加从start到end的数组元素
    public CalTask(int[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        if (end - start < THRESHOLD) {
            for (int i = start; i < end; i++) {
                sum += arr[i];
            }
            return sum;
        } else {
            CalTask left = new CalTask(arr, start, (start + end) / 2);
            CalTask right = new CalTask(arr, (start + end) / 2, end);
            left.fork();
            right.fork();
            //把两个小任务的结果累加起来.
            return left.join() + right.join();
        }
    }

    public static void main(String[] args) throws Exception {
        int[] arr = new int[100];
        Random rand = new Random();
        int total = 0;
        //初始化100个数字元素
        for (int i = 0; i < arr.length; i++) {
            total += (arr[i] = rand.nextInt(20));
        }
        System.out.println(total);
        //创建一个通用池
        ForkJoinPool pool = ForkJoinPool.commonPool();
        //提交任务,获得结果
        ForkJoinTask<Integer> future = pool.submit(new CalTask(arr, 0, arr.length));
        System.out.println(future.get());
    }
}

线程相关类

ThreadLocal类

该类为每一个使用该变量的线程都提供一个变量值的副本.使每一个线程都可以独立的改变自己的副本.
ThreadLocal类提供了如下三个public方法.
T get():返回此线程局部变量中当前线程副本中的值.
void remove(): 删除此线程局部变量中当前线程的值
void set(T value): 设置此线程局部变量中当前线程副本中的值.

包装线程不安全的集合.

Collection类有以下几个静态方法可以把collection包装成线程安全的集合.
synchronizedCollection(Collection c)
synchroizedList(List list)
Map Set 类似.

信号量Semaphore

信号量提供了两种构造函数:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)//第二个参数可以指定是否公平
主要逻辑方法有
public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
acquire()方法尝试获得一个准入许可,若无法获得,则线程会等待.release()方法用于在线程访问资源后释放一个许可,以使其他等待许可的线程可以进行资源访问.

ReadWriteLock 读写锁

读的时候允许其他人读,不允许其他人写.
写的时候不允许任何人读和写.获得读写锁的方法也很简单.
new 一个 ReentrantLock(),然后用它来获取readLock和writeLock();

CountDownLatch

当倒计时计数器用.
构造函数: public CountDownLatch(int count)
示例代码(模拟火箭发射):

public class CountDownLatchDemo implements Runnable {
    static final CountDownLatch end = new CountDownLatch(10);
    static final CountDownLatchDemo demo = new CountDownLatchDemo();
    @Override
    public void run() {
         try {
            //模拟检查任务
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("check complete");
            end.countDown();
         } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 10; i < 10; i++) {
            exec.submit(demo);
        }
        //等待检查.十个倒计时结束才执行后面的内容.
        end.await();
        //发射火箭
        System.out.println("Fire!");
        exec.shutdown();
    }
}

大概的使用方法就是,我有一个countDownLatch对象.他有一个计数器.
线程们可以给他倒计时.然后倒计时结束就可以执行后面的程序了.

锁的优化

减少锁持有时间

只对需要同步竞争的代码块加锁,其他代码块不加锁.

减少锁粒度

缩小锁定对象的范围,降低锁冲突的可能性.进而提高系统的并发能力.

用读写分离锁来替代独占锁.

锁分离

咱俩没竞争关系,咱俩就别用一个锁.

锁粗化

减少加锁解锁的次数.

JVM的努力

锁偏向

如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无须在做任何同步操作.这样可以节省大量锁申请的操作.

轻量级锁

如果偏向锁失败,就会使用轻量级锁的优化手段.
它只是简单的将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁,,如果线程获得轻量级锁成功,则可以顺利进入临界区.如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁申请就会膨胀为重量级锁.

自旋锁

锁膨胀后,为了避免线程真实的在操作系统层面挂起,虚拟机还会做最后的努力--自旋锁. 虚拟机会让当前线程做几个空循环,在经过若干次循环后,如果可以得到锁,就顺利进入临界区,如果还不能获得锁,才会真的把线程在操作系统层面挂起.

锁消除

有一些没必要用锁的地方,就去掉锁.

无锁

使用CAS操作来避免冲突.
CAS(V,E,N) V表示要更新的变量,E表示预期值,N表示新值.仅当V等于E时,才会将V设为N,如果不等,那么说明已经有线程做了更新.则当前线程什么都不做. 最后CAS返回当前V的真实值.整个操作是原子操作,所以不会被中断.

无锁的线程安全整数 AtomicInteger

他是整数,同时线程安全,任何操作都是用CAS指令进行的.
AtomicInteger类有以下方法:
public final int get() 取得当前值
public final void set(int newValue) 设置当前值
public final int getAndSet(int newValue) 设置新值,并返回旧值
public final boolean compareAndSet(int expect, int u) 如果当前值为expect,则设置为U
public final int getAndIncrement() 当前值加一,返回旧值.
public final int getAndDecrement() 当前值减一,返回旧值.
public final int getAndAdd(int delta) 当前值增加delta,返回旧值
public final int incrementAndGet() 当前值加一,返回新值.
public final int decrementAndGet 当前值减一,返回新值.
public final int addAndGet(int delta) 当前值增加delta,返回新值
AtomicInteger的使用十分简单,示例代码如下:

public class AtomicIntegerDemo {
    static AtomicInteger i = new AtomicInteger(0);

    public static class AddThread implements Runnable {
        public void run() {
            for (int k = 0; k <1000; k++) {
                //把i加一
                i.incrementAndGet();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException{
        Thread[] ts = new Thread[10];
        for (int k = 0; k < 10; k++) {
            ts[k] = new Thread(new AddThread());
        }
        for (int k = 0; k < 10; k++) {
            ts[k].start();
        }
        for (int k = 0; k < 10; k++) {
            ts[k].join();
        }
        System.out.println(i);
    }
}

在 incrementAndGet()这个方法里,会有一个无线循环.如果操作没成功,会一直尝试操作,直到成功为止.

无锁的对象引用:AtomicReference

AtomicReference与AtomicInteger非常类似,但是后者是对整数的封装,而前者则是封装普通的对象引用.他可以保证我们在修改对象引用时的线程安全性.他后面带泛型就完事儿啦.很简单,不多说.

带有时间戳的对象引用 AtomicStapmpedReference,他维护了一个时间戳,每次修改值都会对应修改时间戳.

无锁数组AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

分别对应了整数,长整数和引用对象数组.
他们有如下方法:

public final int get(int i); //获得数组第i个下标的元素
public final int length();  //获得数组的长度
public final int getAndSet(int i, int newValue); //将数组第i个下标设置为newvalue,并返回旧值
public final boolean compareAndSet(int i, int expect, int update);
//进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true.
public final int getAndIncrement(int i);//第i个值加一
public final int getAndDecrement(int i); //第i个值减一
public final int getAndAdd(int i, int delta);//第i个值增加delta  

让普通变量也能享受原子操作.AtomicIntegerFiledUpdater AtomicLongFiledUpdater AtomicReferenceFiledUpdater 这三类.分别对应整数,长整数,和引用类.

示例代码如下:

public class AtomicIntegerFieldUpdaterDemo {
    public static class Candidate {
        int id;
        volatile int score;
    }

    public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
    AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
    //检查Updater是否正确工作.
    public static AtomicInteger allScore = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException{
        final Candidate stu = new Candidate();
        Thread[] t = new Thread[10000];
        for (int i = 0; i < 10000; i++) {
            t[i] = new Thread() {
                public void run() {
                    if (Math.random() > 0.4) {
                        scoreUpdater.incrementAndGet(stu);
                        allScore.incrementAndGet();
                    }
                }
            };
            t[i].start();
        }
        for (int i = 0; i < 10000; i++) {
            t[i].join();
        }
        System.out.println("score = " + stu.score);
        System.out.println("allScore = " + allScore);
    }
}

避免死锁的几个常见方法

  1. 避免一个线程同时获得多个锁。
  2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  3. 尝试使用定时锁,使用tryLock(timeout)来替代使用内部锁机制。
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

volatile

可以保证共享变量的可见性。还可以保证指令不重排。

处理器实现原子操作

简单的原子操作可以直接保证,复杂的原子操作需要用到总线锁和缓存锁。

总线锁

使用处理器提供的一个LOCK#信号,当一个处理器输出此信号时,其他处理器的请求将被阻塞,,那么该处理器可以独占共享内存。

缓存锁

保证对缓存访问的独占性

volatile具有以下特性

  1. 可见性,对一个volatile变量的读,总是能看到任意线程对这个volatile变量的最后的写入。
  2. 原子性,对任意单个volatile变量的读/写具有原子性,但类似于volatile++这样的复合操作不具有原子性。

线程的生命周期


等待锁(synchronized和lock)都是block的状态. wait, join, sleep这种都是waiting状态. wait完可能还要继续等待block, 因为没拿到锁. wait()会释放锁, sleep不会释放锁.

为什么需要interruptexception?

线程在wait, sleep,和jion的时候,是拿不到CPU的,也就没法把自己的interrupt这个标志位置为true.所以要抛出异常,调度器将该线程的interrupt标志位手动置为true. 而且如果自己的写的线程没有对interrupt这个标志位的判断的话, 那么这个中断是没啥用的. 只有你写了判断逻辑,才可能真正的中断线程. 自己的一个误区: wait, 锁, 中断. 这是三种东西, 基本没联系的. 不要强行联系在一起. wait, sleep,join这种更类似于一种线程间通信的手段, 锁才是真正防止竞争用的. 中断就是单纯的标志位, 需要自己处理,只是wait, sleep,join这三个方法是响应中断,会抛出中断异常. 如果在等待锁的线程, 就是 synchronized和lock的代码, 是不响应中断的, 因为线程本身就是block的.

posted @ 2019-12-24 17:32  时光轻轻吹  阅读(299)  评论(0编辑  收藏  举报