CAS的ABA问题
使用CAS编写代码,比较然后交换。
大部分没问题,但在极端情况下有问题。
比如银行取钱一个人账户是1000,取款500,假设是按照CAS的方式来执行的。按取款卡了一下,又按了一下。就有两个线程来进行操作,如果恰好有人转账500,则会出现bug。
如何处理ABA问题呢?使用账户余额判定,本身就不科学。属于能加也能减。引入一个只能加的版本号,每次取钱或者存钱,版本号就+1,就可以进一步解决ABA问题。CAS的核心就在于说,通过数值没变=》中间没有别的线程覆盖。
JUC中的常见类
java.util.concurrent 放了和多线程相关的组件。
callable接口(也是描述一个任务,call有返回值)Runnable描述一个任务没有返回值(run方法是返回void)
比如对一个值增加的时候
package Thread;public class Demo26 {public static int sum=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(new Runnable() {@Overridepublic void run() {int result=0;for (int i = 1; i <= 1000; i++) {result+=i;}sum=result;}});t1.start();t1.join();System.out.println(sum);}}
这串代码的耦合非常高
如果运用Callable接口的话
package Thread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo27 {public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<Integer> callable=new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result=0;for (int i = 1; i <= 1000; i++) {result+=i;}return result;}};FutureTask<Integer> futureTask=new FutureTask<>(callable);Thread t=new Thread(futureTask);t.start();t.join();System.out.println(futureTask.get());}
}
这串代码就降低了耦合
futureTask就相当于发票一样。
ReentrantLock(可重入锁)
ReentrantLock的价值:
1.ReentrantLock提供了公平锁的实现。
synchronized只是非公平锁
ReentrantLock locker=new ReentrantLock(true);
写true就是公平形态。写false或者不写就是非公平锁。
2..ReentrantLock提供tryLock操作,给加锁提供了更多的课操作空间。
尝试加锁,如果锁已经被获取到了,直接返回失败,而不会继续等待。
synchronized都是遇到锁竞争,就阻塞等待(死等)
tryLock除了直接返回这种形态之外,还有一个版本,可以指定等待超时时间。
3.synchronized 是搭配wait notify等待通知机制。
ReentrantLock是搭配Condition类完成等待通知。
Condition要比wait notify更强一点(多个线程wait,notify是唤醒随机的一个,Conditon指定线程唤醒)
信号量Semaphore
信号量就是一个计数器
信号量有两个基本操作
1,P操作
2,V操作
CountDownLatch(相对来说比较实用的工具类)
当我们把一个任务拆分分成很多个的时候,就可以通过这个工具类来识别任务是否整体执行完毕了
package Thread;import java.util.concurrent.CountDownLatch;public class Demo28 {public static void main(String[] args) throws InterruptedException {CountDownLatch latch =new CountDownLatch(10);for (int i = 0; i < 10; i++) {int id=i;Thread t=new Thread(() ->{System.out.println("线程启动"+id);try{//假设这里是进行一些下载操作Thread.sleep(3000);}catch(InterruptedException e){throw new RuntimeException(e);}System.out.println("线程结束"+id);latch.countDown();});t.start();}//通过await等待所有的线程调用countDownlatch.await();System.out.println("所有线程结束");}
}
await会阻塞等待,一直到countDown调用的次数。和构造方法指定的次数一致的时候,await才会返回。
CopyOnWriteArraylist也是一种解决线程问题安全的做法(写时拷贝)
如果当前有多个线程去读,此时不需要做任何处理。
假设某个线程要对上面的数据进行修改。修改的同时,很多的线程在读呢,如果直接修改,不加任何处理,意味着有的线程会有中间情况。
写完之后,用新的数组的引用,代替旧的数组的引用。
(引用赋值操作,是原子的)
上述过程,没有任何加锁和阻塞等待,也能确保线程不会读出“错误的数据”,旧的空间可以释放了。
多线程使用队列
直接使用BlockkingQueue即可。
多线程使用哈希表.
HashMap是线程不安全的,Hashtable是带锁的,但不推荐使用。
标准库提供了更好的替代,ConcurrentHashMap
ConcurrentHashMap做出了优化
1.使用“铁桶”的方式,来替代“一把全局锁”,有效降低锁冲突的概率。
2.像hash表的size,即使你插入的元素是不同的俩表==链表上的元素,也会设计到多线程修改同一个变量。
引入CAS,通过CAS的方式,来修改size,也就避免了加锁操作。
3.针对扩容操作做了特殊优化。
如果发现,负载因子太大了,就需要扩容,扩容是一个比较重量比较低效的操作。
普通的hashmap要在一次put的过程中,完成整个扩容过程。
就会使put操作非常ka。
coucurrentHashmap会在扩容的时候,搞两份空间。
一份是之前扩容之前的空间。
一份是扩容之后的空格。
接下来每次进行hash表的基本操作,都会把一部分数据从旧空间搬运到新空间。