使用Lock锁解决线程安全问题牌【重点】
Lock lock = new ReentrantLock();
lock.lock();
需要同步的代码(需要保证原子性的代码)
lock.unlock();volatile关键字和synchronized关键字的区别
volatile 能解决有序性和可见性
原子类 能解决变量操作的原子性(有序性和可见性)
synchronized 能解决多句代码的原子性(有序性和可见性)CopyOnWriteArrayList类的作用
代替多线程的情况,线程安全的ArrayList集合
CopyOnWriteArraySet类的作用
代替多线程的情况,线程安全的HashSet集合
ConcurrentHashMap类的作用
代替多线程的情况,线程安全的HashMap集合(比HashTable效率更好)
CountDownLatch类的作用
可以允许一个线程等待另外一个线程执行完毕后再继续执行
CyclicBarrier类的作用
让一组线程都到达某种条件后再执行某个任务
Semaphore类的作用
控制多线程并发的最大数量
Exchanger类的作用
用于线程间的通信(数据交换)
一。synchronized关键字
1.多行代码的原子性问题:
AtomicInteger类只能保证 变量 的原子性操作,而对多行代码进行原子性操作,使用AtomicInteger类就达不到效果了。
即 无法解决 多行代码的原子性。
多行代码的原子性安全问题:卖票案例:
卖票案例:
/**
* 卖票任务
*/
public class TicketTask implements Runnable{
//定义变量,表示初始有100张票
int count = 100;
@Override
public void run() {
while (true){
if (count > 0) {
//先判断,后卖票
try {Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("卖出第"+count+"张票!");
//票数要减少
count--;
}else {
break;
}
}
}
}
public class TestDemo {
public static void main(String[] args) {
//0.创建卖票任务
TicketTask tt = new TicketTask();
//1.创建卖票窗口
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);
//2.开始卖票
t1.start();
t2.start();
t3.start();
//线程出现安全问题:
//a.可能出现重复数据
//b.可能出现0,甚至-1这种非法数据
}
}
出现了线程安全问题(原子性问题):
//a.可能出现重复数据
原因: 当一个线程执行完卖票后,还没有来得及对票数-1,被其他线程抢走了CPU,导致其他线程也卖出一样的票
//b.可能出现0,甚至-1这种非法数据
原因: 当剩下最后一张票时,多线程都经过了if判断 进入卖票的代码块中,然后依次卖出第1张,第0张,第-1
synchronized 关键字介绍
synchronized 是java提供的关键字
作用:可以让多句代码具有原子性。(当某个线程执行多句代码的过程中不被其他线程打断)
解决方案一:
同步代码块
格式:
synchronized(锁对象){
需要同步的代码(需要保持原子性的代码)
}
解决代码:
/**
* 卖票任务
*/
public class TicketTask implements Runnable{
//定义变量,表示初始有100张票
int count = 100;
Object obj = new Object();
@Override
public void run() {
while (true){
//同步代码块
synchronized (obj){
if (count > 0) {
//先判断,后卖票
System.out.println("卖出第"+count+"张票!");
//票数要减少
count--;
}
}
}
}
}
解决方案二:
同步方法:
格式:
public synchronized void 方法名(){
需要同步的代码(需要保证原子性的代码)
}
解决代码:
/**
* 卖票任务
*/
public class TicketTask implements Runnable{
//定义变量,表示初始有100张票
int count = 100;
@Override
public void run() {
while (true){
sellTicket();
}
}
public synchronized void sellTicket(){
if (count > 0) {
//先判断,后卖票
System.out.println("卖出第"+count+"张票!");
//票数要减少
count--;
}
}
}
注意:
a.同步代码块和同步方法其实原理是差不多,同步代码块的同步锁我们需要自己指定,而同步方法的同步锁
,默认使用当前对象this
b.同步方法能否是静态方法呢? 可以是static的,如果同步方法是static的,
默认使用当前类的字节码文件作为锁对象
解决方案三
Lock锁
Lock是一个接口,我们需要使用其实现类 ReentrantLock
ReentrantLock的API:
public ReentrantLock();
public void lock(); //加锁
public void unlock();//解锁(释放锁)
格式:
ReentrantLock lock = new ReentrantLock();
lock.lock(); //加锁
需要同步的代码块(需要保持原子性的代码)
lock.unlock();//解锁
解决代码:
/**
* 卖票任务
*/
public class TicketTask implements Runnable{
//定义变量,表示初始有100张票
int count = 100;
//创建一个Lock锁
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
//加锁
lock.lock();
if (count > 0) {
//先判断,后卖票
System.out.println("卖出第"+count+"张票!");
//票数要减少
count--;
}
lock.unlock();
}
}
}
二,并发包
这是JDK提供的,包含一个在高并发情况使用集合或者工具,使用这些集合或者工具类时,能保证高并发情况下是安全的
1.CopyOnWriteArrayList
ArrayList 是线程不安全的
public class MyThread extends Thread {
public static List<Integer> list = new ArrayList<>();//线程不安全的
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
System.out.println("添加完毕!");
}
}
public class TestArrayList {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("最终集合的长度:" + MyThread.list.size());
}
}
CopyOnWriteArrayList是线程安全的
public class MyThread extends Thread {
// public static List<Integer> list = new ArrayList<>();//线程不安全的
public static List<Integer> list = new CopyOnWriteArrayList<>();//保证线程安全
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
System.out.println("添加完毕!");
}
}
2.CopyOnWriteArraySet
HashSet是线程不安全的
public class MyThread extends Thread {
public static Set<Integer> set = new HashSet<>();//线程不安全的
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
set.add(i);
}
System.out.println("添加完毕!");
}
}
public class TestSet {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
t1.start();
//主线程也添加10000个
for (int i = 10000; i < 20000; i++) {
MyThread.set.add(i);
}
Thread.sleep(1000 * 3);
System.out.println("最终集合的长度:" + MyThread.set.size());
}
}
CopyOnWriteArraySet是线程安全的
public class MyThread extends Thread {
//public static Set<Integer> set = new HashSet<>();//线程不安全的
public static Set<Integer> set = new CopyOnWriteArraySet<>();//线程安全的
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
set.add(i);
}
System.out.println("添加完毕!");
}
}
3.ConcurrentHashMap
HashMap是线程不安全的
public class MyThread extends Thread {
public static Map<Integer, Integer> map = new HashMap<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
map.put(i, i);//0,0 1,1 2,2 3,3 ... 9999,9999
}
}
}
public class TestMap {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
t1.start();
for (int i = 10000; i < 20000; i++) {
MyThread.map.put(i, i);
}
Thread.sleep(1000 * 2);
System.out.println("map最终大小:" + MyThread.map.size());
}
}
Hashtable是线程安全的,但效率低
public class MyThread1 extends Thread {
public static Map<Integer, Integer> map = new Hashtable<>(); //线程是安全
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
map.put(i, i);
}
long end = System.currentTimeMillis();
System.out.println((end - start) + " 毫秒");
}
}
public class TestMap1 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new MyThread1().start();//开启1000个线程
}
Thread.sleep(1000 * 20);//由于每个线程执行时间稍长,所以这里多停顿一会
System.out.println("map的最终大小:" + MyThread1.map.size());
}
}
ConcurrentHashMap既安全又效率高
public class MyThread1 extends Thread {
// public static Map<Integer, Integer> map = new Hashtable<>(); //线程是安全,但是效率低
public static Map<Integer, Integer> map = new ConcurrentHashMap<>(); //线程是安全,但是效率高
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
map.put(i, i);
}
long end = System.currentTimeMillis();
System.out.println((end - start) + " 毫秒");
}
}
小结:
HashMap 线程不安全(多线程不能操作同一个HashMap)
HashTable 线程安全的(多线程可以操作同一个HashTable),但是效率较低
ConcurrentHashMap 线程安全的,并且效率比较高
为什么Hashtable效率低而ConcurrentHashMap效率高?
因为HashTable对哈希表进行全表加锁
而ConcurrentHashMap只对某个桶(链表)局部加锁,并且也同时使用CAS机制
4.CountDownLatch
允许当前线程,等待其他线程完成某种操作之后,当前线程继续执行
构造方法:
public CountDownLatch(int count); 需要传入计数器,需要等待的线程数
成员方法:
public void await() throws InterruptedException// 让当前线程等待
public void countDown() // 计数器进行减1
需求:
线程1要执行打印:A和C,线程2要执行打印:B
我们需要这样的结果: 线程1 先打印A 线程2打印B 之后 线程1再打印C
A B C
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
//0.创建一个CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
//1.创建两个线程
Thread t1 = new MyThread1(latch);
Thread t2 = new MyThread2(latch);
t1.start();
Thread.sleep(5000);
t2.start();
}
}
public class MyThread1 extends Thread {
private CountDownLatch latch;
public MyThread1(CountDownLatch latch){
this.latch = latch;
}
@Override
public void run() {
System.out.println("A....");
try {
latch.await();//让当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C....");
}
}
public class MyThread2 extends Thread {
private CountDownLatch latch;
public MyThread2(CountDownLatch latch){
this.latch = latch;
}
@Override
public void run() {
System.out.println("B....");
//让latch的计数器减少1
latch.countDown();
}
}
5.CyclicBarrier
让多个线程,都到达了某种要求之后,新的任务才能执行
构造方法:
public CyclicBarrier(int parties, Runnable barrierAction);
需要多少个线程 所有线程都满足要求了,执行的任务
成员方法:
public int await();当某个线程达到了,需要调用await()
需求: 部门开会,假设部门有五个人,五个人都到达了才执行开会这个任务
public class TestPersonThread {
public static void main(String[] args) throws InterruptedException {
//0.创建一个CyclicBarrier
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("人都齐了,开会吧");
}
});
//1.创建五个线程
PersonThread p1 = new PersonThread(barrier);
PersonThread p2 = new PersonThread(barrier);
PersonThread p3 = new PersonThread(barrier);
PersonThread p4 = new PersonThread(barrier);
PersonThread p5 = new PersonThread(barrier);
//2.开启
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
//Thread.sleep(6000);
//System.out.println("人都到了,开会吧...");
//要求,人没到不开会,都到了立刻开会!!!
}
}
public class PersonThread extends Thread {
private CyclicBarrier barrier;
public PersonThread(CyclicBarrier barrier){
this.barrier = barrier;
}
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(6)*1000);
System.out.println(Thread.currentThread().getName() + " 到了! ");
//调用 barrier的await 表示线程到了
try {
barrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
补充:
Math的静态方法
public static double random(); //获取一个0(包括)到1(不包括)的正小数
6.Semaphore
用于控制并发线程的数量!!
构造方法:
public Semaphore(int permits); //参数 permits 表示最多允许有多少个线程并发执行
成员方法:
public void acquire(); //获取线程的许可证
public void release(); //释放线程的许可证
public class MyThread extends Thread {
private Semaphore semaphore;
public MyThread(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run(){
//从Semaphore获取线程的许可
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 进入 时间=" + System.currentTimeMillis());
try {
Thread.sleep(100*new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 结束 时间=" + System.currentTimeMillis());
//归还semaphore线程的许可
semaphore.release();
}
}
public class TestDemo {
public static void main(String[] args) {
//0.创建Semaphore
Semaphore semaphore = new Semaphore(3);
//最多的并发线程数量为1
for (int i = 0; i < 10; i++) {
new MyThread(semaphore).start();
}
}
}
7.Exchanger
用于线程间的数据交换
构造方法:
public Exchanger<V>();
成员方法:
public V exchange(V x);//参数为发给别的线程的数据,返回值别的线程发过来的数据
public class TestExchanger {
public static void main(String[] args) throws InterruptedException {
//0.创建一个线程间数据交互对象
Exchanger<String> exchanger = new Exchanger<String>();
//1.创建线程A
ThreadA aThread = new ThreadA(exchanger);
aThread.start();
//休眠
Thread.sleep(5000);
ThreadC cThread = new ThreadC(exchanger);
cThread.start();
}
}
public class ThreadA extends Thread {
private Exchanger<String> exchanger;
public ThreadA(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程A,要将礼物AAA,送给线程C...");
//调用exchanger
String result = null;
try {
result = exchanger.exchange("AAA");//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程A,获取到线程C的礼物:"+result);
}
}
public class ThreadC extends Thread {
private Exchanger<String> exchanger;
public ThreadC(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程C,要将礼物CCC,送给线程A...");
String result = null;
try {
result = exchanger.exchange("CCC");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程C,获取到线程A的礼物:"+result);
}
}
其他:
使用多线程是为了程序效率的需求,所以不能通过不使用多线程的方式实现,而是应该使用线程同步技术解决线程安全问题;
线程安全问题必须是多个线程对同一个数据发生了修改操作;如果多个线程对同一个数据的操作是读取操作,没有修改操作,则一定不会出现线程安全问题
同步代码块并没有增加程序效率,反而降低了程序的效率。多线程情况下,遇到同步代码,多个线程会依次进入。
某公司组织年会,会议入场时有两个入口,在入场时每位员工都能获取一张双色球彩票,假设公司有100个员工, 利用多线程模拟年会入场过程,并分别统计每个入口入场的人数,以及每个员工拿到的彩票的号码。线程运行后打印 格式如下:
编号为: 2 的员工 从后门 入场! 拿到的双色球彩票号码是:[17, 24, 29, 30, 31, 32, 07]
编号为: 1 的员工 从后门 入场! 拿到的双色球彩票号码是:[06, 11, 14, 22, 29, 32, 15]
//.....
从后门入场的员工总共: 13 位员工
从前门入场的员工总共: 87 位员工
===
双色球的工具类
import java.util.Arrays;
import java.util.Random;
public class DoubleColorBallUtil{
// 产生双色球的代码
public static String create() {
String[] red = {"01","02","03","04","05","06","07","08","09","10",
"11","12","13","14","15","16","17","18","19","20","21","22","23",
"24","25","26","27","28","29","30","31","32","33"};
//创建蓝球
String[] blue = "01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16".split(",");
boolean[] used = new boolean[red.length];
Random r = new Random();
String[] all = new String[7];
for(int i = 0;i<6;i++) {
int idx;
do {
idx = r.nextInt(red.length);//0‐32
} while (used[idx]);//如果使用了继续找下一个
used[idx] = true;//标记使用了
all[i] = red[idx];//取出一个未使用的红球
}
all[all.length-1] = blue[r.nextInt(blue.length)];
Arrays.sort(all);
return Arrays.toString(all);
}
}
线程安全:
public class LuckThread implements Runnable {
// 员工人数
private int number = 100;
public void run() {
// 获得线程的名字
String name = Thread.currentThread().getName();
// 定义变量统计人数
int count = 0;
// 开始进场抽奖,
while (true) {
synchronized (this) {
// 首先判断number,大于0才能抽奖
if (number > 0) {
// 使用工具类生成一个彩票号码给这个员工
String dc = DoubleColorBallUtil.create();
// 输出抽中的彩票号
System.out.println("编号为: " + number + " 的员工 从"+name+"入场! 拿到的双色球彩票号码是: " + dc);
// 进入一个员工,少一个员工
number--;
// 计数加一
count ++;
// 休眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 抽奖完毕,大于前后门入场的员工人数
System.out.println("从" + name + "入场的员工总共: " + count + " 位员工");
break;
}
}
}
}
}
public class Demo01 {
public static void main(String[] args) {
// 创建抽奖任务,实现了Runnable接口
LuckThread ld = new LuckThread ();
// 在主线程中开启两个线程,表示前门和后门
// 传入实现Runnable接口的实现类对象
Thread t1 = new Thread(ld, "前门");
Thread t2 = new Thread(ld, "后门");
// 开启两个线程
}