多线程相关
多线程实现使用方式:
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来处理多线程问题。
浙公网安备 33010602011771号