Java线程实现与安全

1、线程的实现
一、线程的三种实现方式  
  首先并发并非咱们一般咱们认为的必须依靠线程才能实现,可是在Java中并发的实现是离不开线程的,线程的主要实现有三种方式:java

使用内核线程(Kernel Thread,KLT)实现
使用用户线程实现sql

使用用户线程加轻量级进程混合实现数据库

  (1)使用内核线程(Kernel Thread,KLT)实现:安全

  直接由OS(操做系统)内核(Kernel)支持的线程,程序中通常不会使用内核线程,而是会使用内核线程的高级接口,即轻量级进程(Light Weight Process,LWP),也就是一般意义上的线程。多线程

每一个轻量级线程与内核线程之间1:1的关系称之为一对一的线程模型。架构

  优势:每一个LWP是一个独立调度单元,即便阻塞了,也不会影响整个进程。并发

  缺点:须要在User Mode与Kernel Mode中来回切换,系统调用代价比较高;因为内核线程的支持会消耗必定的内核资源,所以一个系统支持轻量级进程的数量是有限的。分布式

  (2)使用用户线程实现:ide

  广义上来讲,一个线程只要不是内核线程就能够认为是用户线程(User Thread,UT),但其实现仍然创建在内核之上;狭义上来讲,就是UT是指彻底创建在用户空间的线程库上,Kernel彻底不能感到线程的实现,线程的全部操做彻底在User Mode中完成,不须要内核帮助(部分高性能数据库中的多线程就是UT实现的)高并发

  缺点:全部的线程都须要用户程序本身处理,以致于“阻塞如何解决”等问题很难解决,甚至没法实现。因此如今Java等语言中已经抛弃使用用户线程。

  优势:不须要内核支持

  (3)使用用户线程加轻量级进程混合实现:

  内核线程与用户线程一块儿使用的实现方式,而OS提供支持的轻量级进程则是做为用户线程与内核线程之间的桥梁。UT与LWP的数量比是不定的,是M:N的关系(许多Unix系列的OS都提供M:N的线程模型)

二、Java线程的实现与调度
  (1)Java线程的实现:

  OS支持怎样的线程模型,都是由JVM的线程怎么映射决定的。

  在Sun JDK中,Windows与Linux都是使用一对一的线程模型实现(一条Java线程映射到一条轻量级进程之中);

  在Solaris平台中,同时支持一对一与多对多的线程模型

  (2)Java线程调度:

  是指系统内部为线程分配处理使用权的过程,主要调度分为两种,分别是协同式线程调度和抢占式线程调度。

    1)协同式调度:线程执行时间由线程自己控制,线程工做结束后主动通知系统切换到另外一个线程去。    

      ① 缺点:线程执行时间不可控,切换时间不可预知。若是一直不告诉系统切换线程,那么程序就一直阻塞在那里。

      ② 优势:实现简单,因为是先把线程任务完成再切换,因此切换操做对线程本身是可知的。

    2)抢占式调度:线程执行时间由系统来分配,切换不禁线程自己决定,Java使用就是抢占式调度。而且能够分配优先级(Java线程中设置了10中级别),但并非靠谱的(优先级可能会在OS中被改变),这是由于线程调度最终被映射到OS上,由OS说了算,因此不见得与Java线程的优先级一一对应(事实上Windows有7中,Solairs中有2的31次方)

2、线程安全
一、Java中五种共享数据
  (1)不可变:典型的final修饰是不可变的(在构造器结束以后),还有String对象以及枚举类型这些自己不可变的。

  (2)绝对线程安全:无论运行时环境如何,调用者都不须要任何额外的同步措施(一般须要很大甚至不切实际的代价),在Java API中不少线程安全的类大多数都不是绝对线程安全,好比java.util.Vector是一个线程安全容器,它的不少方法(get()、add()、size())方法都是被synchronized修饰,可是并不表明调用它的时候就不须要同步手段了。

  (3)相对线程安全:就是咱们一般说的线程安全,Java API中不少这样的例子,好比HashTable、Vector等。

  (4)线程兼容:就是咱们一般说的线程不安全的,须要额外的同步措施才能保证并发环境下安全使用,好比ArrayList和HashMap

  (5)线程对立:无论采用何种手段,都没法在多线程环境中并发使用。

二、线程安全的实现方法
(1)互斥同步(Mutual Exclision & Synchronization)

  同步:保证同一时刻共享数据被一个线程(在使用信号量的时候也能够是一些线程)使用。

  互斥:互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥手段。

  1)Java中最经常使用的互斥同步手段就是synchronized关键字,synchronized关键字通过编译后会在代码块先后生成monitorenter(锁计数器加1)与monitorexit(锁计数器减1)字节码指令,而这两个指令须要一个引用类型参数指明要锁定和解锁的对象,也就是synchronized(object/Object.class)传入的对象参数,若是没有参数指定,那就看synchronized修饰的是实例方法仍是类方法,去取对应的对象实例与Class对象做为锁对象。

  Java线程要映射到OS原生线程上,也就是须要从用户态转为核心(系统)态,这个转换可能消耗的时间会很长,尽管VM对synchronized作了一些优化,但仍是一种重量级的操做。

  2)另外一个就是java.util.concurrent包下的重入锁(ReentrantLock),与synchronized类似,都具备线程重入(后面会介绍重入概念)特性,可是ReentrantLock有三个主要的不一样于synchronized的功能:

    等待可中断:持有锁长时间不释放,等待的线程能够选择先放弃等待,改作其余事情。

    可实现公平锁:多个线程等待同一个锁时,是按照时间前后顺序依次得到锁,相反非公平锁任何一个线程都有机会得到锁。

    锁绑定多个条件:是指ReentrantLock对象能够同时绑定多个Condition对象。

  JDK 1.6以后synchronized与ReentrantLock性能上基本持平,可是VM在将来改进中更倾向于synchronized,因此在大部分状况下优先考虑synchronized。

(2)非阻塞同步

  1)“悲观”并发策略------非阻塞同步概念

    互斥同步主要问题或者说是影响性能的问题是线程阻塞与唤醒问题,它是一种“悲观”并发策略:老是会认为本身不去作相应的同步措施,不管共享数据是否存在竞争它都会去加锁。

    而相反有一种“乐观”并发策略,也就是先操做,若是没有其余线程使用共享数据,那操做就算是成功了,可是若是共享数据被使用,那么就会一直不断尝试,直到得到锁使用到共享数据为止(这是最经常使用的策略),这样的话就线程就根本不须要挂起。这就是非阻塞同步(Non-Blocking Synchronization)

    使用“乐观”并发策略须要操做和冲突检测两个步骤具备原子性,而这个原子性只能靠硬件完成,保证一个从语义上看起来须要屡次操做的行为只经过一条处理器指令就能完成。经常使用的指令有:测试并设置(Test-and-Set)、获取并增长(Fetch-and-Increment)、交换(Swap)、比较并交换(Compare-and-Swap,CAS)、加载连接/条件储存(Load-Linked/Store-Conditional,LL/SC)

  2)CAS介绍

有三个操做数,分别是内存位置V,旧的预期值A和新值B,CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,不然不更新,可是都会返回V的旧值,整个过程都是一个原子过程。

               

                 

    以前我在Java内存模型博文中介绍volatile关键字的在高并发下并不是安全的例子中,最后的结果并非咱们想要的结果,可是在java.util.concurrent整数原子类( 如AtomicInteger)中,compareAndSet()与getAndIncrement()方法使用了Unsafe类的CAS操做。如今咱们将int换成AtomicInteger,结果都是咱们所期待的10000

复制代码
1 package cas;
2 /*
3
Atomic 变量自增运算测试
4 @author Lijian
5

6 /
7 import java.util.concurrent.ExecutorService;
8 import java.util.concurrent.Executors;
9 import java.util.concurrent.TimeUnit;
10 import java.util.concurrent.atomic.AtomicInteger;
11
12 public class CASDemo {
13
14 private static final int THREAD_NUM = 10;//线程数目
15 private static final long AWAIT_TIME = 5
1000;//等待时间
16 public static AtomicInteger race = new AtomicInteger(0);
17
18 public static void increase() { race.incrementAndGet(); }
19
20 public static void main(String[] args) throws InterruptedException {
21 ExecutorService exe = Executors.newFixedThreadPool(THREAD_NUM);
22 for (int i = 0; i < THREAD_NUM; i++) {
23 exe.execute(new Runnable() {
24 @Override
25 public void run() {
26 for (int j = 0; j < 1000; j++) {
27 increase();
28 }
29 }
30 });
31 }
32 //检测ExecutorService线程池任务结束而且是否关闭:通常结合shutdown与awaitTermination共同使用
33 //shutdown中止接收新的任务而且等待已经提交的任务
34 exe.shutdown();
35 //awaitTermination等待超时设置,监控ExecutorService是否关闭
36 while (!exe.awaitTermination(AWAIT_TIME, TimeUnit.SECONDS)) {
37 System.out.println("线程池没有关闭");
38 }
39 System.out.println(race);
40 }
41 }
复制代码
经过观察incrementAndGet()方法源码咱们发现:

复制代码
public final int getAndIncrement() {
for(;;){
int current = get();
int next = current+1;
if(compareAndSet(current, next)) {
return current;
}
}
}
复制代码

经过for(;;)循环不断尝试将当前current加1后的新值(mext)赋值(compareAndSet)给本身,若是失败的话就从新循环尝试,值到成功为止返回current值。  

  3)CAS的ABA问题

    这是CAS的一个逻辑漏洞,好比V值在第一次读取的时候是A值,即没有被改变过,这时候正要准备赋值,可是A的值真没有被改变过吗?

    答案是不必定的,由于在检测A值这个过程当中A的值可能被改成B最后又改回A,而CAS机制就认为它没有被改变过,这也就是ABA问题,解决这个问题就是增长版本控制变量,可是大部分状况下ABA问题不会影响程序并发的正确性。

(3)无同步方案

  “要保障线程安全,必须采用相应的同步措施”这句话其实是不成立的,由于有些自己就是线程安全的,它可能不涉及共享数据天然就不须要任何同步措施保证正确性。主要有两类:

  1)可重入代码(Reentrant Code)

    也就是常常所说的纯代码(Pure Code),能够在任什么时候刻中断它,以后转入其余的程序(固然也包括自身的recursion)。最后返回到原程序中而不会发生任何的错误,即全部可重入的代码都是线程安全的,而全部线程安全的代码都是可重入的

    其主要特征是如下几点:

    ① 不依赖存储在堆(堆中对象是共享的)上的数据和公用的系统资源(方法区中能够共享的数据。好比:static修饰的变量,类的能够相关共享的数据),能够换句话说就是不含有全局变量等;

    ② 用到的状态由参数形式传入;

    ③ 不调用任何非可重入的方法。

  便可以以这样的原则来判断:咱们若是能预测一个方法的返回结果而且方法自己是可预测的,那么输入相同的数据,都会获得相应咱们所期待的结果,就知足了可重入性的要求。

  2)线程本地存储(Thread Lock Storage)

    若是一段代码中所须要的数据必须与其余代码共享,那么能保证将这些共享数据放到同一个可见线程内,那么无须同步也能保证线程之间不存在竞争关系。

    在Java中若是一个变量要被多线程访问,可使用volatile关键字修饰保证可见性,若是一个变量要被某个线程共享,能够经过java.lang.ThreadLocal类实现本地存储的功能。每一个线程Thread对象都有一个ThreadLocalMap(key-value, 欢迎工做一到五年的Java工程师朋友们加入Java群: 891219277群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!ThreadLocalHashCode-LocalValue),ThreadLocal就是当前线程ThreadLocalMap的入口。

相关文章
相关标签/搜索