public class ReenterLock implements Runnable{
public static ReentrantLock t1 = new ReentrantLock();
public static int i = 0;
public void run(){
for(int j=0;j<1000;j++){
t1.lock();
try{
i++;
}finally{
t1.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock relock = new ReenterLock();
Thread t1 = new Thread(relock);
Thread t2 = new Thread(relock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
公平锁
公平锁的原则就是两个线程谁先来,谁先使用资源。不会产生饥饿的现象,只要排队,都可以被执行。
但是synchronized本身就是强制破坏了这种规则,但是重入锁有这个机制,其有一个构造函数。
public ReentrantLock(boolean fair)
如果fair是true的话,就是一个公平锁,例如
public static ReentrantLock t1 = new ReentrantLock(true);
重入锁实现的三要素:
1:原子状态,原子状态使用CAS操作来存储当前锁的状态,判难断锁是否已经被别的线程持有。
2:是等待队列,所有没有请求到锁的线程,会进入等待队列进行等待,持有线程的锁释放后,系统就能从等待队列唤醒一个线程,继续工作。
3:阻塞原语park()和unpark(),同来挂起和恢复线程,没有得到锁的线程将会被挂起。
重入锁的搭档Condition
Condition的作用和wait和notify的作用大致相同,但是wait和notify是搭配synchronized使用,但是Condition是搭配重入锁使用。
await方法会使得当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行,当线程被中断的时候,也能跳出等待。
singal()方法用于唤醒一个在等待的线程,相对的singalAll()方法会唤醒所有的等待中的线程。
public class ConditionTest implements Runnable{
public static ReentrantLock relock = new ReentrantLock();
public static Condition condition = relock.newCondition();
public void run(){
try{
relock.lock();
condition.await();
System.out.println("Thread is bbq");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
relock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionTest con = new ConditionTest();
Thread thread = new Thread(con);
thread.start();
Thread.sleep(2000);
relock.lock();
condition.signal();
relock.unlock();
}
}
改程序实现了Condition,在重入锁两次锁定的时候,await()会让对象放弃锁,signal会让对象重新获取锁。
允许多个线程同时访问:信号量
无论是synchronized还是重入锁,都是只能允许一个线程访问一个资源。而信号量可以指定多个线程,同时访问一个资源。
public Semaphore(int permits)
public Semaphore(int permits,boolean fair) //第一个参数是线程的个数,第二次但是是表示是否公平。
读写锁ReadWriteLock
读写锁允许多个线程同时读,使得线程之间能够并行。
读-读不互斥:读读之间不阻塞
读-写互斥:读阻塞写,写也会阻塞读
写-写互斥,写写阻塞
在系统中,读操作次数远大于写操作的时候,则读写锁就可以发挥最大的功效。
倒计时器:CountDownLatch和循环栅栏:CyclicBarrier
倒计时器是一个非常强大的多线程控制器,比如在放飞火箭的时候,必须先确保各种仪器的检查,引擎才能点火,这种情况就适合使用CountDownLatch。
public CountDownLatch(int count)//构造器接受的这个参数就是这个计时器的计数个数
循环栅栏是另外一种多线程并发控制工具,和倒计时器的作用类似,但是功能比倒计时器强大而且复杂。
线程阻塞工具类:LockSupport
它可以让线程在任意位置让线程阻塞,和Thread.suspend()相比,它弥补了由于resume先发生,导致线程无法执行的情况和wait相比,也不需要先获得某个对象的锁。不会抛出InterruptedException异常。
LockSupport中的静态方法sprk可以阻塞当前线程。
线程池
线程的缺点:创建和关闭需要花费大量的时间,大量的线程会抢占宝贵的内存资源,处理不当,就会很可能造成Out of Memory异常。大量线程回收也会给GC带来很大的压力,大量的线程容易拖垮系统应用。
为了避免频繁的创建和销毁线程,可以让创建的线程反复的使用,不用急着关闭线程,而是让他还给线程池,可以节约不少创建和销毁对象的时间。
线程池的实现:
核心的线程池都实现了ThreadPoolExecutor,里面具体的参数有:
corePoolSize:指定了线程池中的线程数量
maximumPoolSize:指定了线程池的最大线程数量
keepAliveTime:当现场超过corePoolSize的时候,多余的空闲线程的存活时间,也就是超过corePoolSize的空间线程池的存活时间有多长
unit:keepAliveTime的单位
workQueue:任务队列,提交但是没有被执行的任务
threadFactory:线程工程,用于创建线程
handler:拒绝策略,当任务太多的时候,如何拒绝任务。
参数workQueue指被提交但是没有被执行的任务,他是一个BlockingQueue接口的对象,用于存放Runnable对象,可以在ThreadPoolExecutor的构造函数可以使用的阻塞队列如下几种:
直接提交队列
有界的任务队列:有界的任务队列可以用ArrayBlockingQueue实现,ArrayBlockingQueue的构造函数必须带一个容量参数,表示该队列的最大容量,当使用有界队列的时候,若有新任务,如果线程池的实际线程数小于corePoolSize,则会有点创建新的线程,若大于corePoolSize,则会将新任务加入等待队列,若队列已经满了,无法加入,则在中那个线程数不大于maximumPoolSize的前提下,创建新的线程执行任务,若大于maximumPoolSize,则执行拒绝策略,
无界的任务队列
有限任务队列
jdk的四种拒绝策略
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
CallerRunsPolicy策略:只要线程没有关闭,该策略直接在调用者线程中,运行当先被丢弃的任务
DiscardOledestPolicy策略:该策略将丢弃最老的一个请求,也就是丢弃即将被执行的一个任务,并且尝试再此提交当前任务。
DiscardPolicy策略:该策略默默丢弃无法处理的任务,不予任何处理
以上的策略都是实现了RejectedExecutionHandler接口,如果以上的策略都不能满足实际情况,可以自己扩展RejectedExecutionHandler接口。
public interface RejectedExecutionHandler{
void rejectedExecution(Runnable r,ThreadPoolExecutor executor);
}
r是请求执行的任务,executor是当前的线程池。
ThreadFactory(自定义线程池):最开始的线程都是从ThreadFactory来的,
扩展线程池一般有beforeExecute()、afterExecute()、terminated()三个接口对线程池扩展,一般可以通过设置这些,捕获任务的异常。
优化线程池的数量:在《java Concurrency in Practice》中给出了估算线程池大小的经验公式:
Nthreads = Ncpu*Ucpu*(1+W/C)
Ncpu = CPU的数量,在就java中,可以通过:Runtime.getRuntime().availableProcessors()获取
Ucpu = 目标CPU的使用率,0<=Ucpu<=1
W/C = 等待时间与计算时间的比率
通常会出现隐藏堆栈异常的错误,所以我们一般可以修改提交代码到线程池的方法
正常的:
for(int i=0i<5;i++){
pools.submit(new DivTask(100,i));
}
改成pools.execute(new DivTask(100,i)); 或者Future re = pools.submit(new DivTask(100,i)); re.get();这两种都可以得到部分堆栈信息。
大量数据,分而治之:fork/join框架
思想:把一个大任务分成若干个小任务,每次处理1/若干个小任务,然后在把所有的任务进行合并join,若其中出现线程A把自己的任务做完了,线程B还没有,则线程A会帮助线程B,从线程B拿一个任务,尽可能的达到平衡,但是A拿任务的时候是从队列的底部拿数据,而线程执行自己的任务的时候是从队列的顶部拿任务,这样就有利于避免数据竞争。
jdk的并发容器
hashMap的线程安全的做法:
第一种就是使用Collections.synchronizedMap()方法包装HashMap,
public static Map m = Collecctions.synchronizedMap(new HashMap());
另外一个方法就是ConcurrentHashMap,这是更适合于多线程的场合。
List线程安全就是public static List = Collections.synchronizedList(new LinkedList());
高效读写队列:ConcurrentLinkedQueue对内部的Node操作的时候,是进行的CAS操作
高效读取:CopyOnWriteArrayList,适用于读操作远大于写操作,在写入操作的时候,进行一次自我复制,也就是在修改的时候不修改原来的内容,而是对原有数据进行一次复制,将修改后的内容复制到副本中,操作完后,在将副本数据替换原来数据。
跳表:跳表是快速查找的数据结构,跳表的插入和删除只是需要对整个数据结构进行局部的调整,所以在高并发的情况下,只需要进行部分锁就行,时间复杂度O(logn),跳表的本质是维护了多条链表,而且链表分层,最底层的链表维护了链表的所以元素,上面每一层都是下面一层的子集,查找时从顶部开始找。
跳表实现的Map和哈希算法实现的Map的区别:哈希并不会保存元素的顺序,但是跳表在所有的元素都是有序的,跳表中除了key和Value之外,每一个Node还hi指向下一个Node,因此还有一个元素next,而且对元素Node的所有操作都是CAS方法。