多线程相关

多线程实现使用方式

1.实现Runnable接口(函数式接口)

Runnable r = ()->{task code}

Thread t = new Thread(r);

t.start();

2.继承Thread类(每个任务都会建立新的进程,开销很大,目前不建议使用,一般使用线程池)

Class MyThread extends Thread{

public void run(){

taks code;

}

}

注意事项

(1)Thread.run只是运行当前线程,不能启动新线程,所以要启动新线程使用start。

(2)调用线程休眠sleep方法的时候要捕捉InterruptedException异常。在中断阻塞中的线程的时候会抛出这个异常。

(3)没有可以强制线程终止的方法。只能使用interrupt方法设置中断标志位。每个线程不时的检查这个标志位。

线程状态:

1.New(新创建) 2.Runnable(可运行)3.Blocked(被阻塞)4.Waiting(等待)5.Timed waiting(计时等待) 6.Terminated(被终止)

注意事项:

1.阻塞状态和等待状态有很大差别。

2.阻塞状态:最常见的是请求被其他线程占有的锁的时候,线程会进入阻塞状态。当锁被释放后,调度器就可以调度这个线程执行,也就是变成非阻塞状态。

3.等待状态:调用wait或者join方法,需要等待其他线程notify或者notifyAll否则会一直等待下去。

                  join方法用来等待线程终止,可以用来进行线程排序,比如有三个线程,要依次执行,就可以使用join方法进行设置。

                  wait,notify,notifyAll使用的是object中的条件变量 monitor.

 

线程同步

1.synchronized

Java中的关键字

优点:

(1)jvm进行加锁解锁操作,不用显示调用。

缺点:

(1)不能中断一个正在试图获取锁的线程。

(2)无法设置获取锁的超时时间,也就是如果获取不到锁就会一直等下去。

(3)只有一个条件对象

2.lock ReentrantLock类

优点:

(1)提供中断和超时时间的构造方法,可以设置中断相应和超时中断。

(2)可以设置多个条件对象。

(3)对代码块进行加锁,粒度更细。

(4)提供try方式,可以对资源试加锁。

缺点:

(1)需要显示调用的加锁方式,使用时要注意必须手动释放锁。

3.条件对象

当线程获取到对象锁后,发现有些其他条件不满足无法继续执行,这个时候就使用条件对象。

比如说银行扣款操作,类Bank,当一个线程获取到Bank的锁后,发现余额不足无法完成扣款,必须等到其他线程把余额增加后才可以。但是,这个时候该线程占着这个锁,其他线程无法进行增加余额的操作。

这个时候就可以在Bank中声明一个Condition对象C,如果发现余额不足就调用对象C的await()方法。这样当前线程就会放弃Bank的锁,并且进入C的等待集。当其他线程增加了余额后调用C的signalAll后,当前线程就可以继续竞争Bank的锁并进行扣款操作了。

4.同步阻塞

synchronized(obj){

critical section

}

每个Java对象都有一个锁,这里是使用这个锁来同步代码块。

5.Volatile

Java关键字,为实例域的同步访问提供了一种免锁的机制。当变量的值被一个线程修改后,会使得其他线程对该变量的缓存失效,必须重新从主存中获取。

(1) 让变量的修改在线程直接可见。

(2) 禁止指令重排序。

缺点:无法保证原子性,比如a++这种操作一定要注意使用同步方法保证原子性。

注意事项:

1.上面两个都是获取对象锁

2.ReentrantLock提供可重入锁,在线程获取了改对象的锁后,在调用对象中的其他同步方法时不用再去获取锁,可以直接使用,底层实现上来说会把锁的引用计数加一。引用计数为0的时候锁释放。

3.signal是随机通知等待线程中的一个。

4.锁的选择顺序:无锁->synchronized->lock

 

CAS无锁同步方式

加锁实现同步的方式会大大降低系统的吞吐量,为了提高吞吐量Java提供了一种原子的无锁方式进行字段值的修改。

唯一的乐观锁,乐观的认为不用加锁就可以保证线程直接同步的正确性。

原理:如果要给变量赋值,先判断变量现在的值是否跟预期的值相同,如果相同就把变量的值改成要修改的值,如果不相同就不修改,重新计算要设置的值继续尝试修改。

        上面的判断和设置使用原子的方式,要么都执行成功,要么都执行失败。从而保证多线程直接的正确性。

相关类:

AtomicInteger,AtomicIntegerArray,AtomicReference等,

使用方法:

do{

    oldValue = largest.get();

    newValue = Math.max(oldValue,observed);

} while(!largest.compareAndSet(oldValue, newValue));

其中compareAndSet方法会映射到一个处理器操作,保证原子性,一般是用本地方法实现的。调用Unsafe中的方法。

问题:当有大量线程要访问相同的原子值,由于一直在重试导致性能大大下降。

方案:LongAdder  使用增加因子,每次有值需要增加的时候把需要增加的值记录下来,最后统一增加。

 

线程局部变量

ThreadLocal类,辅助各个线程的操作,可以为每个线程提供一个自己的实例。可以把一下线程不安全的方法或变量放到里面,以避免多线程问题,比如 SimpleDateFormat.

实例:

要获取当前时间,可以在局部变量中每次创建一个新的SimpleDateFormat对象,但是这样会很浪费资源。如果使用一个静态方法类获取又会有多线程问题。

方案:

public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInital(()->new SimpleDateFormat('yyyy-MM-dd'));

使用上面的方法为每个线程创建一个对象,在使用的时候

String dateStamp = dateFormat.get().format(new Date());

其中第一次调用get的时候会先调用initialValue方法,后面就会返回当前线程的实例。一个线程局部变量只能放一个对象,如果要使用其他的需要先删除到这个。

Java给随机数的获取设置了一个专门的类 ThreadLocalRandom。

注意事项:

(1)在获取时间的方案中,有可能一个线程会多次创建dateFormat,比如后面换成了其他对象,现在又需要获取当前时间。为了保证每个线程只创建一次,可以声明一个map把线程当前局部变量缓存进行。

线程安全容器 

ConcurrenctHashMap

数据结构:数组链表结构,不同的key可能会hash成相同的值从而放到相同的位置,这个时候就使用链表进行存储。这个结构跟HashMap是一致的。

特殊结构:为了提高并发性能,把数组分成了很多segment,每个segment持有一个锁,每次操作只获取字段在的segment的锁即可。

               segment中包含一个entry数组,存放具体的键值对。

               segment继承了ReentraceLock类来进行同步处理。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
  transient volatile int count; //Segment中元素的数量
  transient int modCount; //对table的大小造成影响的操作的数量(比如put或者remove操作)
  transient int threshold; //阈值,Segment里面元素的数量超过这个值那么就会对Segment进行扩容
  final float loadFactor; //负载因子,用于确定threshold
  transient volatile HashEntry<K,V>[] table; //链表数组,数组中的每一个元素代表了一个链表的头部
}

效率提升:

(1)当有很多key的hash值相同时会产生链表,在获取链表中的值的时候就需要进行便利会导致效率低下。为此ConcurrenctHashMap在链表数量超过8个后会把阶段设置为一个红黑树,来提高查询效率。

(2)jdk1.8中进行了去锁优化,使用CAS和synchronized来处理多线程问题。

 

posted on 2019-08-02 11:25  云无形  阅读(114)  评论(0)    收藏  举报