android 线程知识你还记得多少?

845 阅读4分钟

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

常见使用线程的几种方式

  • 方式一: 请添加图片描述

  • 方式二:

  • 方式三:

方式一,方式二和方式三的启动:

  public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式一
        MyThread myThread = new MyThread();
        myThread.start();

        // 方式二
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();

        // 方式三
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<String>(myCallable) {
            @Override
            protected void done() {
                System.out.println("方式三开始前!");
                super.done();
                System.out.println("方式三执行后!");
                try {
                    System.out.println("done内部获取方式三返回结果:" + get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };
        // 方式三启动线程
        new Thread(futureTask).start();

        System.out.println("获取方式三返回结果:" + futureTask.get());
}

运行结果: 请添加图片描述 三种方式的区别:

  • extend Thread 需要手动重写run方法
  • implements Runnable 需要将当前runnable对象传入Thread
  • implements Callable<*> 可以获取到thread的返回值,并且不能直接传给thread启动,需要采用FutureTask来配合,并且可以通过FutureTask.get()获取到实现 implements Callable的<*> call的返回值.

Thread.start() 和 Thread.run() 的区别

再来看一段代码: 请添加图片描述

  • start:
    Client4Thread client4Thread = new Client4Thread();
    client4Thread.start();
    
    运行结果:
    thread:Thread-0
    
  • run:
    Client4Thread client4Thread = new Client4Thread();
    client4Thread.run();
    
    运行结果:
    thread:main
    

结论: 如果调用Thread.run()方法相当于一个普通类调用了一个run方法,并没有开启子线程

相当于这样:

public static class User {
  public void run() {
         System.out.println("User.run" + Thread.currentThread().getName());
     }
 }

client4Thread.run()等价于User.run()!

Client4Thread client4Thread = new Client4Thread();
//        client4Thread.start();

// 效果一样
client4Thread.run();
//
//
new User().run();

Thread.join() 线程串形化

先来看使用场景:

请添加图片描述 现在有三个线程,同时运行,先来运行看看结果:

请添加图片描述 可以看出,每次执行顺序都不同..

如果你想让他同步执行,你可以这样做:

请添加图片描述 结论: join的作用就是将线程串行化,需要注意的是,join()一定要放在现场开启(start())后,

Thread.Interrupt() 线程中断

请添加图片描述 运行代码: 请添加图片描述 运行结果为: 请添加图片描述 可以看出,虽然向子线程发送了中断信号,但是线程并不会执行终端,而是一直存活着.

如果想要线程中断,只需要在Thread中判断一下isInterrupted()即可

例如这样:请添加图片描述 运行结果为: 请添加图片描述 小坑: 如果你需要在!isInterrupted()中休眠一下,例如这样: 请添加图片描述 这样是不可以的,因为Thread.sleep会捕获中断信号,并且将中断信号改为false

最终导致结果成这样了

请添加图片描述

最终就会导致捕获到了中断异常,但是Thread.sleep() 擅自更改了.. 只需要在捕获到InterruptedException异常的时候,再次更改状态即可:

例如这样: 请添加图片描述 当然不仅是extend Thread这种写法可以中断线程,implements Runnable当前也可以中断线程,比如这样写:

请添加图片描述 效果是一样的,就不重复展示了..

Thread 数据共享 synchronized(隐式锁)

还是先来看简单的案例:

现在有2个线程,需要同时对count ++ ,例如这样: 请添加图片描述 结果为: 请添加图片描述 可以看出,每次运行的结果都不一样?

这是为什么?

因为这里是循环添加 1000 * 100次,在2个线程同时跑的时候,就会出现两个count同时执行的情况,所以导致数字总是不太对.

这里只需要让他同步起来(添加同步锁)即可,例如这样:

请添加图片描述 常见锁的写法:

  • 锁某一个类的class:

    private static final Object object = new Object();
    
    public static void addCount() {
        for (int i = 0; i < countLength; i++) {
            synchronized (object) {
                count++;
            }
        }
    }
    
  • 锁当前对象的 this

    public synchronized void addCount2() {
       	.....
    }
    
  • 锁指定对象的class:

    public static void addCount() {
      for (int i = 0; i < countLength; i++) {
               synchronized (Client2.class) {
                 count++;
             }
         }
     }
    

Thread线程隔离(ThreadLocal)

什么是线程隔离,还是先看代码: 请添加图片描述 还是先来看结果: 请添加图片描述 可以看出,每一次的运行效果都是不一样的

这是因为启动(start)的时候,线程只是就绪状态,只是通知JVM我可以执行了,真正的执行是JVM来调用的

以最后一次结果来看问题:

ThreadName:Thread-1	threadIndex:1	count:2
ThreadName:Thread-4	threadIndex:4	count:11
ThreadName:Thread-3	threadIndex:3	count:7
ThreadName:Thread-2	threadIndex:2	count:4
ThreadName:Thread-0	threadIndex:0	count:2

先来分析一下代码:

main方法中,开启了5个线程,并且将下标传了进去,然后再MyThreadLocal具体对下表累加

需求分析:

既然说到是线程隔离,那么想要的效果应该是假如现在传入进来的是5,那么通过

count+=threadIndex;

应该结果是6,而不应该吧其他线程的结果也算到当前线程头上

这段话说的比较绕,直接来看ThreadLocal就明白了!

请添加图片描述 启动的时候添加了一个Thread.join() 使线程顺序执行..

直接看效果: 请添加图片描述

可以看出,现在就实现了线程的隔离

如果你阅读过Handler源码,也会发现他内部使用了ThreadLocal来进行线程隔离

不过这里线程隔离隔离的是Lopper,为了保证一个线程只有一个looper

瞟一眼handler的代码: 在这里插入图片描述

notify(),notifyAll()和wait()的使用

tips:notify() notifyAll() 和wait是Object()的属性,所以每一个类都可以调用

还是通过一段代码来看他们的区别:

  • WaitAndNotifyBean: 请添加图片描述

    通过调用waitName(),来实现等待 通过changeName() 来实现刷新

  • MyThread: 请添加图片描述

  • 使用: 请添加图片描述 来看看效果:

notifynotifyAll

可以看出,好像并没有什么区别,notifynotifyAll 都能够唤醒wait后的线程

接下来,加大剂量!

notifynotifyAll

可以看到,这下效果就很明显了!

多个线程wait的情况下

  • notify ,只会刷新一个awit,其他线程继续等待
  • notifyAll,会刷新所有的wait!

tips:notifyAll notify() 和 wait()都需要放到里面!

显示锁

可重入锁 (ReentrantLock)

什么是可重入?

int count = 0;
public synchronized void test() {
     count++;
     if (count <= 10) {
         test();
     }
 }

先拿synchronized来举例,以当前的认知来看,如果递归情况下,synchronized一定会被锁死,因为第一次执行test()的时候,还没有释放锁就进行了第二次的加锁

但是从结果来看,毫无问题,因为synchronized是具有可重入性的,可重入就是指如果同一个实例的话,可重新进入

ReentrantLock 也是同样的道理

来看看ReentrantLock的使用: 请添加图片描述 结果还是那个结果: 请添加图片描述 这里需要注意的是,在使用隐式锁的时候,一定要放在try { ... } finally { .. }代码块中

这样做是因为即使抛出异常,也会吧锁释放掉,不会出现死锁情况

规范示例:

lock.lock();
try{
    count++;
} finally {
    lock.unlock();
}

读写锁 (ReentrantReadWriteLock)

要想突出ReentrantReadWriteLock的效果,那就必须有对比,这里就和synchronized来比较!

思路: 开启3个写线程,30个读线程,来模拟实际开发中的读取操作

通过同步锁(synchronized)和读写锁的实际效果来看他们的运行机制!

在来看代码思路:

  • StudentImpl 请添加图片描述

  • getThread:请添加图片描述

  • setThread: 请添加图片描述 运行: 请添加图片描述

synchronized效果和读写锁对比:

synchronized读写锁

可以看出,效果是十分的明显,从变化中也可以看出,如果要进行大量的读写操作,使用读写锁效率会大大的提高!

课外知识 volatile!

volatile本不属于Thread的知识,但是既然说到这么多关键字,那就提一嘴吧!

还是一段代码引出区别:

未加volatile关键字: 请添加图片描述 效果图:

请添加图片描述 可以看出,不加volatile关键字,isRun,即使是改变了,线程中也是没反应,接受不到..

再来看看添加volatile关键字: 请添加图片描述 效果图: 请添加图片描述 结果显而易见,一旦标记了volatile isRun变量只要发生改变就回直接通知JVM来改变当前的状态!!

完整代码

原创不易,您的点赞就是对我最大的支持!