synchronized和ReentrantLock两种加锁方式
synchronized加锁
public class Ticket implements Runnable {
private int ticketNum = 1000;
private static final Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticketNum <= 0) {
System.out.println("票买完了");
break;
} else {
ticketNum--;
System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩"+ticketNum+"张票");
}
}
}
}
}
ReentrantLock加锁
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private int ticketNum = 1000;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticketNum <= 0) {
System.out.println("票买完了");
break;
} else {
ticketNum--;
System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩"+ticketNum+"张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
死锁问题
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
java 死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
等待唤醒机制
等待唤醒机制需要用到Object中的方法:
- void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
- void notify() 唤醒正在等待对象监视器的单个线程(随机唤醒一个)。
- void notifyAll() 唤醒正在等待对象监视器的所有线程。
wait()方法和sleep()方法的区别
- sleep来自Thread类,和wait来自Object类
- sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”
- 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用并且由锁对象调用,而sleep可以在任何地方使用
synchronized(x){
x.notify()
//或者x.wait()
}
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
阻塞队列
在Collection下面有一个接口Queue(队列),Queue有一种实现就是阻塞队列BlockingQueue,BlockingQueue有两个实现类:
- ArrayBlockingQueue:数组形式的队列
- LinkedBlockingQueue:链表形式的队列
构造函数:
ArrayBlockingQueue(int capacity) 创建具有给定(固定)容量和默认访问策略的 ArrayBlockingQueue 。
常用方法:
void put(E e)
在该队列的尾部插入指定的元素,如果队列已满,则等待空间变为可用。
E take()
检索并删除此队列的头,如有必要,等待元素可用。
可以看出,这两个方法都有等待机制,想要详细了解的可以去看看源码
数据同步
volatile关键字
在多线程的应该中,有共享数据 int a = 10,然后有A和B两个线程共用这个数据,当A和B两个线程同时运行,他们都会将a的值拷贝一份到缓存中;然后A将a = 9,此时B中的a依然是10。
这时可以使用volatile(volatile int a = 10)关键字,强制每次使用a的时候都会到内存中重新获取a的最新值。
synchronized加锁
当然,上面的问题也可以使用synchronized加锁解决,因为synchronized也会强制每次使用a的时候都会到内存中重新获取a的最新值。
原子性
多线程的原子性:一次或多次操作中,要么所有的任务都完成,要么所有的任务都不执行。
如:两个人吃100个包子,如果不能保证原子性,就可能出现意料之外的问题。
volatile关键字只能保证数据是最新的,但不能保证原子性,因为在拿到最新数据后,可能被打断操作;
synchronized可以保证原子性,因为能保证一次操作中所有任务都执行完。
atomic中的工具类
在java.util.concurrent.atomic包中给我们提供了一组能保证基本数据的原子性的工具类。
这些工具类通过CAS算法和自旋锁保证数据的原子性,即:
- 每次读取数据时会保存数据的旧值;
- 然后去修改数据时,将旧值和内存中的值进行对比,如果旧值和内存中的值相等就进行改变并获取最新的值;
- 如果旧值和内存中的值不相等,则不修改并获取最新的值。
这种比较方式叫做CAS算法;每次不相等时就会进行自旋操作(就是获取最新的值,再进行比较,一个循环)
悲观锁(synchronized)和乐观锁(CAS算法)的区别
悲观锁:当线程使用一个资源时,别的线程只能等待该资源的释放。速度较慢
乐观锁:当线程使用一个资源时,不上锁,别的线程依然能使用这个资源。速度较快
并发工具类
Hashtable
HashMap不是线程安全的,在多线程环境中,一般使用Hashtable。
Hashtable时线程安全的,但是效率低下,因为每次操作会将整个数组加悲观锁。
ConcurrentHashMap
为了改进Hashtable的效率问题,就出现了ConcurrentHashMap,在JDK1.7以前,ConcurrentHashMap数组的每个元素中再存放一个数组,每次操作时,将其中一个元素上锁后(synchronized)再操作其中的数组;JDK1.8以后使用synchronized+CAS对数组中每个元素进行操作,这样效率就比较高了。
CountDownLatch
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助
常用方法:
void await()
导致当前线程等到锁存器计数到零,除非线程是 interrupted 。
void countDown()
减少锁存器的计数,如果计数达到零,释放所有等待的线程。
long getCount()
返回当前计数。
代码示例:
import java.util.concurrent.CountDownLatch;
public class MyThread2 extends Thread {
private final CountDownLatch countDownLatch;
public MyThread2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"我正等带线程1执行完成");
try {
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程1执行完成,我也执行完成了");
}
}
import java.util.concurrent.CountDownLatch;
public class MyThread1 extends Thread {
private final CountDownLatch countDownLatch;
public MyThread1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+"线程执行完成");
}
}
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread thread1 = new MyThread1(countDownLatch);
Thread thread2 = new MyThread2(countDownLatch);
thread1.start();
thread2.start();
}
Semaphore
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。
简单说就是每个程序要获得一个许可证才能执行。
常用方法:
void acquire()
从该信号量获取许可证,阻止直到可用,或线程为 interrupted 。
void release()
释放许可证,将其返回到信号量。
import java.util.concurrent.Semaphore;
public class MyThread1 extends Thread {
private final Semaphore semaphore = new Semaphore(2);
@Override
public void run() {
try {
// 获取通行证
semaphore.acquire();
// 执行代码
System.out.println(Thread.currentThread().getName()+"我正在执行");
// 归还通行证
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}