当前位置 : 主页 > 手机开发 > 其它 >

基础篇(三)

来源:互联网 收集:自由互联 发布时间:2021-06-19
线程 创建线程的方式 继承Thread类 实现Runnable接口 实现Callable接口创建带返回值的线程 通过线程池ExecutorService创建 sleep() 、join()、yield()有什么区别 sleep为线程休眠,使线程进入阻

线程

创建线程的方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口创建带返回值的线程
  • 通过线程池ExecutorService创建

sleep() 、join()、yield()有什么区别

sleep为线程休眠,使线程进入阻塞状态,需要带休眠时间,休眠时间过去后,该线程加入到等待队列等待执行。 使用interrupt()唤醒睡眠中的线程。
join为线程等待,如果在A线程中等待B线程结束再执行,那么再A线程中使用B.join()即可。在join中带时间参数,表明如果在该段时间之内B未结束,那么不在等待。
yield()函数表示线程的让步,只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
wait()也是暂停该线程,和sleep不同的是,会释放锁标志,该线程停止后,其他的线程可以继续运行。只是该线程停住。

CyclicBarrier

在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程。比如这几个类。
适用于这种情况:当你创建一组任务,他们并行的执行,在所有任务执行完毕后,才进行下一步的任务。与CountDownLatch非常类似,但是CountDownLatch只能执行一次,而CyclicBarrier能够多次执行。

CountDownLatch

用来同步一个或者多个任务,强制他们等待其他任务执行的一组操作完成。可以向CountDownLatch中设置初始值,任何在这个对象上调用的wait()都让这个值减1,直至为0;在其他任务都结束自己的工作时候,可以在该对象中调用countDown()来减少其值。

Semaphore

Exchanger

ThreadLocal

为了防止多个线程在共享资源产生冲突,设计了本地线程储存,可以为相同变量的每一个不同线程创建不同的存储。如果有五个线程,都要使用x所表示的对象,那么线程本地储存会生成5个不同的x储存块。通过《THINK IN JAVA》例子进行验证:

public class ThreadLocalDemo implements Runnable {
    private int id;
    ThreadLocalDemo(int id){
        this.id=id;
    }
    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()){
            ThreadLocalMain.increment();
            System.out.println("id"+id+":"+ThreadLocalMain.get());
            Thread.yield();
        }
    }
}

public class ThreadLocalMain {
    private static ThreadLocal<Integer> value=new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return new Random(47).nextInt(1000);
        }
    };
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService=Executors.newCachedThreadPool();
        for(int i=1;i<5;i++) {
            executorService.execute(new ThreadLocalDemo(i));
        }
        TimeUnit.SECONDS.sleep(1);
        executorService.shutdownNow();
    }
    public static void increment(){
        value.set(value.get()+1);
    }
    public static int get(){
        return value.get();
    }
}

线程池的实现原理


当一个任务提交至线程池之后,

  1. 线程池首先判断核心线程池里的线程是否已经满了。如果不是,则创建一个新的工作线程来执行任务。否则进入2.
  2. 判断工作队列是否已经满了,倘若还没有满,将线程放入工作队列。否则进入3.
  3. 判断线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行。如果线程池满了,则交给饱和策略来处理任务。

ThreadPoolExecutor执行execute()流程:
当一个任务提交至线程池之后,

  1. 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.
  2. 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.
  3. 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。

当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.

线程池的作用

在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:

  1. 降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗;
  2. 提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度;
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。

线程安全问题

线程安全问题一般分为三种问题,原子性,可见性,有序性。

  1. 原子性
    在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
    上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i
x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

只有语句1是原子性操作,其他三个语句都不是原子性操作。

  1. 可见性
    对于可见性,Java提供了volatile关键字来保证可见性。
    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
    另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  2. 有序性
    在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
    在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

    volatile实现原理

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    在JVM虚拟机中,线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
    并发编程中存在三个问题,原子性,可见性,有序性。volatile就是为了保证可见性而设计的。volatile不能保证原子性,下面是一个例子:

public class Test {
    public  int inc = 0;
    public  void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                @Override
                public void run() {
                    for(int j=0;j<1000;j++) {
                        test.increase();
                    }
                };
            }.start();
        }
        while(Thread.activeCount()>2)  //保证前面的线程都执行完
        {
            System.out.println("还有"+Thread.activeCount()+"个线程在执行");
            Thread.yield();
        }
        System.out.println(test.inc);
    }
}

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
 “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

synchronized 实现原理

synchronized 与 lock 的区别

两者都是代码块的锁,每次只能执行一个线程,用来控制并发带来的问题。不同之处是synchronized更简单优雅,但是如果在synchronized中带来了故障或异常,通常没有机会修改,但是在lock体中的异常,可以在finally中进行处理。Lock的创建,加锁,释放等都是显式的,将代码块放在try-finally中,finally中释放lock锁。

死锁的条件和实现方式

多个并发进程因争夺系统资源而产生相互等待的现象。

死锁产生的4个必要条件

  1. 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  2. 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  3. 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  4. 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源

简单实现
写两个Thread,一个Main,两个Thread分别带两个锁,锁住的对象是Main中的静态对象s1,s2。使Thread1锁住s1,睡眠一段时间,然后锁住s2,执行程序。Thread2锁住s2,睡眠一段时间,然后锁住s1。两个线程启动,Thread1必定锁住s1,等待s2释放,而Thread锁住s2,等待s1释放,构成了一个死锁。

CAS 乐观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
CAS的原理:

public class CASDemo {
    private volatile int i=0;
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            //比较是否有冲突每次从内存中读取数据然后将此数据和 +1
             后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
            if (compareAndSet(current, next)) {
                return next;
            }
        }
    }
    public final int get(){
        return i;
    }
  ...

在具体的比较中,有个重要的问题是如何保证compareAndSet(current, next)的原子性的,原理类似

if (this == expect) {
     this = update
     return true;
 } else {
     return false;
 }

在上面一段代码里,如果使用Java那么还是会存在多线程的冲突。在这里,CAS通过调用JNI的代码实现的。而compareAndSwapInt就是借助C来调用CPU底层指令实现的。
在编译后的代码里,可以看到还是使用了lock锁的。

ABA 问题

一个内存V中储存了对象A,有两个线程,T1查看该内存V的对象A,并且获取了A的下一个值,这个时候,T2改变了对象A为B,有把B改成了A,这个时候T1在比较原有对象的时候仍为A,这样有可能会出现问题,在单个数值递增的时候,似乎不影响程序的执行。但是在下面的情况中,就会出现错误:

上一篇:AOP的基本认识
下一篇:深浅拷贝
网友评论