学习目标
能够解释安全问题出现的原因
多个线程(无序的)访问同一个共享资源,对同一个资源进行操作,可能出现错误的数据
能够说出volatile关键字的作用
解决变量的:可见性,有序性;不能解决变量的原子性
能够掌握原子类AtomicInteger的使用(重点)
可以解决高并发的原子性
public static AtomicInteger money = new AtomicInteger(0);
money.getAndIncrement();//i++
getAndIncrement内部采用了CAS机制,保证i++三步是一个原子操作
能够理解原子类的工作机制
CAS机制:乐观锁 compareAndSwap
比较并交换(do...while循环)反复比较,只有内存中的值和预期的值一样,才会进行修改,否则就会循环重新获取值
进行下一次比较
能够使用同步代码块解决线程安全问题(重点)
synchronized(锁对象){
访问了共享数据的代码(产生了线程安全问题的代码)
}
注意:必须保证多个线程使用的是同一个锁对象(锁对象放在成员位置)
能够使用同步方法解决线程安全问题(重点)
1.把访问了共享数据的代码,提取出来放到一个方法中
2.在方法上添加一个同步关键字synchronized
权限修饰符 synchronized 返回值类型 方法名(参数){
访问了共享数据的代码(产生了线程安全问题的代码)
}
注意:同步方法使用的锁对象是this
能够说明volatile关键字和synchronized关键字的区别
volatile关键字:只能修饰变量,可以解决变量的可见性,有序性,不能解决原子性
synchronized关键字:不能修饰变量,可以修饰方法,代码块,使用的范围比volatile广,可以解决:可见性,有序性,原子性
能够描述ConcurrentHashMap类的作用(重点)
多线程安全的Map集合(CAS),效率比Hashtable(synchronized)高
第一章 高并发及线程安全
1.高并发及线程安全的概念
1.高并发:在某个时间点上,有多个线程同时访问某一个资源。例如:双十一,12306 , 秒杀
2.线程安全性问题:当多个线程无序的访问同一个资源(例如:同一个变量、同一数据库、同一个文件……),而且访问同一资源的代码不具有“原子性”,这时对这一资源的方法就会产生安全性问题——导致此资源最终的结果是错误。
3.高并发所产生的安全性问题主要表现:
1).可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
2).有序性:即程序执行的顺序按照代码的先后顺序执行。
3).原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
2.高并发问题一_不可见性(重点)
产生的原因:定义一个变量。一个线程修改变量的值,另一个线程由于访问频率太快,导致一直使用本线程区内的变量副本,而没有实时的到主内存中获取变量的新值。
package com.itheima.demo01visible;
public class MyThread extends Thread{
//定义一个供所有的线程共享的静态变量
public static int a = 0;
@Override
public void run() {
System.out.println("Thread-0线程开始执行了,睡眠2秒钟,等待main线程先执行!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread-0线程过了2秒钟之后,开始执行,修改变量a的值为1");
a=1;
System.out.println("Thread-0线程执行线程任务结束了,结束run方法!"+a);
}
}
package com.itheima.demo01visible;
public class Demo01Visible {
public static void main(String[] args) {
//创建MyThread对象,调用start方法开启一个新的线程,执行run方法
MyThread mt = new MyThread();
mt.start();
//主线程在开启新的线程之后,会继续执行main方法中的代码
System.out.println("主线程执行一个死循环!");
while (true){
//判断变量a的值是否为1,是则结束死循环
if(MyThread.a==1){
System.out.println("主线程判断变量a的值为1,结束死循环!");
break;
}
}
}
}
3.Java内存模型JMM(了解)
概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量(成员变量)和类变量(静态成员变量)。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
4.高并发问题二_无序性
1).有序性:多行代码的编写(.java)顺序和编译(.class)顺序是一致的。
有些时候,编译器在编译代码时,为了提高效率,会对代码“重排”:
.java文件
int a = 10; //第一行
int b = 20; //第二行
int c = a * b; //第三行
在执行第三行之前,由于第一行和第二行的先后顺序无所谓,所以编译器可能会对“第一行”和“第二行”进行代码重排:
.class
int b = 20;
int a = 10;
int c = a * b;
2).但在多线程环境下,这种重排可能是我们不希望发生的,因为:重排,可能会影响另一个线程的结果,所以我们不需要代码进行重排
5.高并发问题三_非原子性(重点)
原子:不可分割 100行代码是一个原子,线程执行100行代码不可以分开执行,要么都执行,要都不执行
需求:
1.定义多线程共享的静态变量money
2.Thread-0线程把money的值增加10000
3.main线程把money的值增加10000
4.查看money的最终结果
package com.itheima.demo02Atomic;
public class MyThread extends Thread{
//1.定义多线程共享的静态变量money
public static int money=0;
@Override
public void run() {
//2.Thread-0线程把money的值增加10000
System.out.println("Thread-0线程开始执行了,开始修改money的值");
for (int i = 0; i < 10000; i++) {
money++;
}
System.out.println("Thread-0线程执行线程任务结束了,结束run方法!");
}
}
package com.itheima.demo02Atomic;
public class Demo01Atomic {
public static void main(String[] args) throws InterruptedException {
//创建MyThread对象,调用start方法,开启一个新的线程执行run方法
MyThread mt = new MyThread();
mt.start();
//main线程在开启新的线程之后,会继续执行main方法中的代码
System.out.println("main线程开启改变变量money的值!");
//3.main线程把money的值增加10000
for (int i = 0; i < 10000; i++) {
MyThread.money++;
}
System.out.println("main线程修改money的值结束,睡眠2秒钟,等待Thread-0线程执行完毕!");
Thread.sleep(2000);
//4.查看money的最终结果
System.out.println("两个线程都执行完毕,打印变量money的值:"+MyThread.money);
}
}
main线程开启改变变量money的值!
main线程修改money的值结束,睡眠2秒钟,等待Thread-0线程执行完毕!
Thread-0线程开始执行了,开始修改money的值
Thread-0线程执行线程任务结束了,结束run方法!
两个线程都执行完毕,打印变量money的值:20000
main线程开启改变变量money的值!
Thread-0线程开始执行了,开始修改money的值
main线程修改money的值结束,睡眠2秒钟,等待Thread-0线程执行完毕!
Thread-0线程执行线程任务结束了,结束run方法!
两个线程都执行完毕,打印变量money的值:19966
原子性原理
每个线程访问money变量,都需要三步:
1).取money的值;
2).将money++
3).将money写回
这三步就不具有“原子性”——执行某一步时,很可能会被暂停(失去了cpu的执行权),执行另外一个线程,就会导致变量的最终结果错误!!!!
第二章 volatile关键字(重点)
1.volatile解决可见性
package com.itheima.demo03volatileVisible;
public class MyThread extends Thread{
//定义一个供所有的线程共享的静态变量
public static volatile int a = 0;
@Override
public void run() {
System.out.println("Thread-0线程开始执行了,睡眠2秒钟,等待main线程先执行!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread-0线程过了2秒钟之后,开始执行,修改变量a的值为1");
/*
变量a被volatile修饰,当变量的值被改变之后,会使变量a的所有的副本都失效
线程在使用变量的值,就必须去主内存中从新获取变量的值
*/
a=1;
System.out.println("Thread-0线程执行线程任务结束了,结束run方法!"+a);
}
}
package com.itheima.demo03volatileVisible;
public class Demo01Visible {
public static void main(String[] args) throws InterruptedException {
//创建MyThread对象,调用start方法开启一个新的线程,执行run方法
MyThread mt = new MyThread();
mt.start();
//主线程在开启新的线程之后,会继续执行main方法中的代码
System.out.println("主线程执行一个死循环!");
while (true){
//判断变量a的值是否为1,是则结束死循环
if(MyThread.a==1){
System.out.println("主线程判断变量a的值为1,结束死循环!");
break;
}
}
}
}
主线程执行一个死循环!
Thread-0线程开始执行了,睡眠2秒钟,等待main线程先执行!
Thread-0线程过了2秒钟之后,开始执行,修改变量a的值为1
主线程判断变量a的值为1,结束死循环!
Thread-0线程执行线程任务结束了,结束run方法!1
2.volatile解决有序性
注意:变量添加了volatile关键字,就不会在进行重排了
3.volatile不能解决原子性
package com.itheima.demo04volatileAtomic;
/*
volatile不能解决原子性
*/
public class MyThread extends Thread{
//1.定义多线程共享的静态变量money
public static volatile int money=0;
@Override
public void run() {
//2.Thread-0线程把money的值增加10000
System.out.println("Thread-0线程开始执行了,开始修改money的值");
for (int i = 0; i < 1000; i++) {
money++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-0线程执行线程任务结束了,结束run方法!");
}
}
package com.itheima.demo04volatileAtomic;
public class Demo01Atomic {
public static void main(String[] args) throws InterruptedException {
//创建MyThread对象,调用start方法,开启一个新的线程执行run方法
MyThread mt = new MyThread();
mt.start();
//main线程在开启新的线程之后,会继续执行main方法中的代码
System.out.println("main线程开启改变变量money的值!");
//3.main线程把money的值增加10000
for (int i = 0; i < 1000; i++) {
MyThread.money++;
Thread.sleep(1);//睡眠是为了提高线程安全问题出现的概率(提高线程抢夺cpu的次数)
}
System.out.println("main线程修改money的值结束,睡眠2秒钟,等待Thread-0线程执行完毕!");
Thread.sleep(3000);
//4.查看money的最终结果
System.out.println("两个线程都执行完毕,打印变量money的值:"+ MyThread.money);
}
}
程序执行额结果: volatile不能把原子性的三步变成一个原子执行
1).取money的值;
2).将money++
3).将money写回
这三步在执行的过程中,还是有可能把其他线程打断的
main线程开启改变变量money的值!
Thread-0线程开始执行了,开始修改money的值
main线程修改money的值结束,睡眠2秒钟,等待Thread-0线程执行完毕!
Thread-0线程执行线程任务结束了,结束run方法!
两个线程都执行完毕,打印变量money的值:14355
第三章 原子类
概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,
多个操作是一个不可以分割的整体(原子)。
比如:从张三的账户给李四的账户转1000元,这个动作将包含两个基本的操作:从张三的账户扣除1000元,给李四的账户增加1000元。这两个操作必须符合原子性的要求,
要么都成功要么都失败。
1.原子类概述
概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
1). java.util.concurrent.atomic.AtomicInteger:对int变量进行原子操作的类。
2). java.util.concurrent.atomic.AtomicLong:对long变量进行原子操作的类。
3). java.util.concurrent.atomic.AtomicBoolean:对boolean变量进行原子操作的类。
这些类可以保证对“某种类型的变量”原子操作,多线程、高并发的环境下,就可以保证对变量访问的有序性,从而保证最终的结果是正确的。
AtomicInteger原子型Integer,可以实现原子更新操作
构造方法:
public AtomicInteger(): 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
成员方法:
int get(): 获取AtomicInteger对象中存储的int值
int getAndIncrement(): i++ 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): ++i 以原子方式将当前值加1,注意,这里返回的是自增后的值。
2.AtomicInteger类的基本使用
AtomicInteger也是一个原子的包装类:里边包含了一个int类型的整数值
package com.itheima.demo05AtomicInteger;
import java.util.concurrent.atomic.AtomicInteger;
/*
AtomicInteger类的基本使用
*/
public class Demo01AtomicInteger {
public static void main(String[] args) {
//创建AtomicInteger对象
AtomicInteger ai1 = new AtomicInteger();
System.out.println(ai1.get());//0
AtomicInteger ai2 = new AtomicInteger(10);
System.out.println(ai2.get());//10
//int getAndIncrement(): i++ 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int a = ai2.getAndIncrement();
System.out.println("a:"+a);//a:10
System.out.println(ai2.get());//11
//int incrementAndGet(): ++i 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int b = ai2.incrementAndGet();
System.out.println("b:"+b);//b:12
System.out.println(ai2.get());//12
}
}
3.AtomicInteger解决变量的原子性(可见性、有序性)(重点)
package com.itheima.demo06AtomicInteger;
import java.util.concurrent.atomic.AtomicInteger;
/*
AtomicInteger解决变量的原子性
*/
public class MyThread extends Thread{
//1.定义多线程共享的静态变量money,数据类型使用AtomicInteger
public static AtomicInteger money= new AtomicInteger(0);
@Override
public void run() {
//2.Thread-0线程把money的值增加10000
System.out.println("Thread-0线程开始执行了,开始修改money的值");
for (int i = 0; i < 1000; i++) {
money.getAndIncrement();//money++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-0线程执行线程任务结束了,结束run方法!");
}
}
package com.itheima.demo06AtomicInteger;
public class Demo01Atomic {
public static void main(String[] args) throws InterruptedException {
//创建MyThread对象,调用start方法,开启一个新的线程执行run方法
MyThread mt = new MyThread();
mt.start();
//main线程在开启新的线程之后,会继续执行main方法中的代码
System.out.println("main线程开启改变变量money的值!");
//3.main线程把money的值增加10000
for (int i = 0; i < 1000; i++) {
MyThread.money.getAndIncrement();//MyThread.money++;
Thread.sleep(1);//睡眠是为了提高线程安全问题出现的概率(提高线程抢夺cpu的次数)
}
System.out.println("main线程修改money的值结束,睡眠2秒钟,等待Thread-0线程执行完毕!");
Thread.sleep(3000);
//4.查看money的最终结果
System.out.println("两个线程都执行完毕,打印变量money的值:"+ MyThread.money.get());
}
}
执行结果:原子性问题已经解决了
main线程开启改变变量money的值!
Thread-0线程开始执行了,开始修改money的值
Thread-0线程执行线程任务结束了,结束run方法!
main线程修改money的值结束,睡眠2秒钟,等待Thread-0线程执行完毕!
两个线程都执行完毕,打印变量money的值:2000
.AtomicInteger的原理_CAS机制(乐观锁)
第四章 synchronized关键字
之前我们讲过的AtomicInteger可以对“int类型的变量”做原子操作。但如果需要将“很多行代码”一起作为“原子性”执行——一个线程进入后,必须将所有代码行执行完毕,其它线程才能进入,可以使用synchronized关键字——重量级的同步关键字。
AtomicInteger:只能解决一个变量的原子性
synchronized:可以解决一段代码的原子性
1.售票案例引发的安全性问题(了解)
2.售票案例的代码实现(重点)
package com.itheima.demo07payTicket;
/*
售票案例的代码实现(重点)
*/
public class RunnableImpl implements Runnable{
//定义一个供所有线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//增加一个死循环,让线程一直卖票
while (true){
//判断ticket是否大于0,大于0在卖票
if(ticket>0){
//卖出每张票需要10毫秒,让程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
ticket--;
}else{
break;
}
}
}
}
package com.itheima.demo07payTicket;
/*
创建3个线程,买同100张票(共享数据)
*/
public class Demo01PayTicket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
程序执行的结果:
...
Thread-0线程正在卖第23张票!
Thread-1线程正在卖第22张票!
Thread-2线程正在卖第22张票!
Thread-0线程正在卖第20张票!
Thread-1线程正在卖第19张票!
Thread-2线程正在卖第19张票!
Thread-0线程正在卖第17张票!
Thread-2线程正在卖第16张票!
Thread-1线程正在卖第15张票!
Thread-0线程正在卖第14张票!
Thread-1线程正在卖第13张票!
Thread-2线程正在卖第13张票!
Thread-0线程正在卖第11张票!
Thread-2线程正在卖第10张票!
Thread-1线程正在卖第9张票!
Thread-0线程正在卖第8张票!
Thread-1线程正在卖第7张票!
Thread-2线程正在卖第6张票!
Thread-0线程正在卖第5张票!
Thread-1线程正在卖第4张票!
Thread-2线程正在卖第4张票!
Thread-0线程正在卖第2张票!
Thread-1线程正在卖第1张票!
Thread-2线程正在卖第0张票!
Thread-0线程正在卖第-1张票!
继承方式实现卖票案例:注意继承的方式票必须是静态的,否则会创建300张
package com.itheima.demo07payTicket;
public class MyThread extends Thread{
//定义一个供所有线程共享的票源
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//增加一个死循环,让线程一直卖票
while (true){
//判断ticket是否大于0,大于0在卖票
if(ticket>0){
//卖出每张票需要10毫秒,让程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
ticket--;
}else{
break;
}
}
}
}
package com.itheima.demo07payTicket;
public class Demo02PayTicket {
public static void main(String[] args) {
MyThread t0 = new MyThread();
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t0.start();
t1.start();
t2.start();
}
}
3.线程安全问题的产生原理(了解-扩展知识点)
4.解决线程安全问题的第一种方式使用同步代码块(重点)
package com.itheima.demo08synchronized;
/*
售票案例出现了线程安全问题
卖出了重复的票和不存在的票
解决线程安全问题的第一种方案:使用同步代码块
格式:
synchronized(锁对象){
可能产生线程安全问题的代码(访问了共享数据的代码)
}
原理:
使用同步代码块把一段代码锁住,只让一个线程进入到代码块中执行卖票的代码
注意:
1.同步代码块使用的锁对象可以是任意的对象
Student s = new Student();Object obj = new Object();String s = "abc"; //底层是一个字符数组
2.必须保证所有的线程使用的是同一个锁对象
*/
public class RunnableImpl implements Runnable{
//定义一个供所有线程共享的票源
private int ticket = 100;
//定义一个锁对象
//Student stu = new Student();
//Object obj = new Object();
String str = "abc";//底层是一个数组 new char[]{'a','b','c'};
//设置线程任务:卖票
@Override
public void run() {
while (true){
//同步代码块
synchronized (str){
if(ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
ticket--;
}else{
break;
}
}
}
}
}
package com.itheima.demo08synchronized;
/*
创建3个线程,买同100张票(共享数据)
*/
public class Demo01PayTicket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
5.同步的原理(了解-扩展知识点)
6.解决线程安全问题的第二种方式:使用同步方法(重点)
package com.itheima.demo09synchronized;
/*
售票案例出现了线程安全问题
卖出了重复的票和不存在的票
解决线程安全问题的第二种方案:使用同步方法
步骤:
1.把访问了共享数据的代码,抽取出来,放到一个方法中
2.给方法添加一个synchronized关键字,这个方法就是一个同步方法
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能产生线程安全问题的代码(访问了共享数据的代码)
}
原理:
使用一个同步方法把代码锁住,只让一个线程进入到方法执行卖票的代码
*/
public class RunnableImpl implements Runnable{
//定义一个供所有线程共享的票源
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
System.out.println("this:"+this);
//增加一个死循环,让线程一直卖票
while (true){
//payTicket();
payTicketStatic();
if(ticket<=0){
break;
}
}
}
/*
定义一个静态的同步方法(了解)
静态的同步方法锁对象是谁?
是this吗? 不是 this是创建对象之后才有的 静态方法优先于对象进入到内存中
是本类的class文件对象(反射) RunnableImpl.class==>唯一
*/
public static /*synchronized*/ void payTicketStatic(){
synchronized (RunnableImpl.class){
//判断ticket是否大于0,大于0在卖票
if(ticket>0){
//卖出每张票需要10毫秒,让程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
ticket--;
}
}
}
/*
1.把访问了共享数据的代码,抽取出来,放到一个方法中
2.给方法添加一个synchronized关键字,这个方法就是一个同步方法
同步方法也是使用一个锁对象,把方法锁住,同步方法的锁对象是谁?
是this(本类对象的引用)
*/
public synchronized void payTicket(){
//判断ticket是否大于0,大于0在卖票
if(ticket>0){
//卖出每张票需要10毫秒,让程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
ticket--;
}
}
/*public void payTicket(){
synchronized (this){
//判断ticket是否大于0,大于0在卖票
if(ticket>0){
//卖出每张票需要10毫秒,让程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
ticket--;
}
}
}*/
}
package com.itheima.demo09synchronized;
/*
创建3个线程,买同100张票(共享数据)
*/
public class Demo01PayTicket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
System.out.println("run:"+run);
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
7.解决线程安全问题的第三方式:使用Lock锁(重点)
package com.itheima.demo10Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
售票案例出现了线程安全问题
卖出了重复的票和不存在的票
解决线程安全问题的第三种方案:使用Lock锁
java.util.concurrent.locks.Lock接口
Lock锁比同步代码块和同步方法使用起来更广泛。
Lock接口中的成员方法:
void lock()获取锁。
void unlock() 释放锁。
java.util.concurrent.locks.ReentrantLock类 implements Lock接口
Lock锁的使用步骤:
1.在成员位置创建ReentrantLock对象
2.在访问共享数据的代码前调用lock方法,获取锁对象
3.在访问共享数据的代码后调用unlock方法,释放锁对象
原理:
使用lock方法和unlock方法把一段代码锁住,只让一个线程进入执行
*/
public class RunnableImpl implements Runnable{
//定义一个供所有线程共享的票源
private int ticket = 100;
//1.在成员位置创建ReentrantLock对象
ReentrantLock l = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
while (true){
//2.在访问共享数据的代码前调用lock方法,获取锁对象
l.lock();
if(ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
ticket--;
}
//3.在访问共享数据的代码后调用unlock方法,释放锁对象
l.unlock();
if(ticket<=0){
break;
}
}
}
}
package com.itheima.demo10Lock;
/*
创建3个线程,买同100张票(共享数据)
*/
public class Demo01PayTicket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
8.CAS与Synchronized
AtomicInteger:只能解决一个变量的原子性
synchronized:可以解决一段代码的原子性
CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
Synchronized是从悲观的角度出发:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。
CAS是从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS这种机制我们也可以将其称之为乐观锁。
🎗经验分享
1. 代码
Runnable实现类
public class Ticket implements Runnable{
int count = 100;
@Override
public void run() {
while (true) {
synchronized (new Object()) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖票:" + count--);
}
}
}
}
}
测试类
public class Demo01Test {
public static void main(String[] args) {
Ticket t = new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
2.出现的问题
使用了同步代码块,但是运行之后依旧发生了线程安全问题
3.问题的分析
如果使用同步代码块,并且要保证线程安全,那么需要使用同一个锁对象,而在现在的程序中,多个线程使用的并不是同一个锁,所以产生了线程安全问题。
4.问题解决办法
让多个线程使用同一个锁对象,比如锁对象使用this
public class Ticket implements Runnable{
int count = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖票:" + count--);
}
}
}
}
}
第五章 并发包
在JDK的并发包java.util.concurrent里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。这些集合和工具类都可以保证高并发的线程安全问题.
1.并发List集合_CopyOnWriteArrayList(重点)
List集合的特点:
1.是一个有序的集合
2.允许存储重复的元素
3.包含一些带索引的方法
1).java.util.concurrent.CopyOnWriteArrayList(类):它是一个“线程安全”的ArrayList,我们之前学习的java.utils.ArrayList不是线程安全的。
2).如果是多个线程,并发访问同一个ArrayList,我们要使用:CopyOnWriteArrayList
需求:
1.创建一个被多个线程共享使用静态的ArrayList集合对象
2.使用Thread-0线程往集合中添加1000个元素
3.使用main线程往集合中添加1000个元素
4.统计集合的长度
package com.itheima.demo11List;
import java.util.ArrayList;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
public class MyThread extends Thread{
//1.创建一个被多个线程共享使用静态的ArrayList集合对象
/*
ArrayList集合是一个多线程不安全的集合:
多线程一起添加数据:1.会丢失一些数据 2.会有索引越界异常
*/
//public static ArrayList<Integer> list =new ArrayList<>();
/*
java.util.Vector<E>:JDK1.0时期的集合(最早期的单列集合)
与新 collection 实现不同,Vector 是同步的。
Vector集合底层采用了synchronized同步机制,可以保证多线程的安全
synchronized是悲观锁,效率低下
*/
//public static Vector<Integer> list = new Vector<>();
/*
java.util.concurrent.CopyOnWriteArrayList<E>:是JDK1.5之后出现了
CopyOnWriteArrayList底层采用了CAS机制,可以保证多线程的安全
CAS机制是乐观锁,效率高
*/
public static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
@Override
public void run() {
//2.使用Thread-0线程往集合中添加1000个元素
System.out.println("Thread-0线程开始执行线程任务,往集合中添加元素!");
for (int i = 0; i < 1000; i++) {
list.add(i);//[0,1,2,3....999]
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-0执行for循环结束,执行线程任务结束!");
}
}
package com.itheima.demo11List;
public class Demo01List {
public static void main(String[] args) throws InterruptedException {
//创建MyThread对象,调用start方法,开启一个新的线程,执行run方法
MyThread mt = new MyThread();
mt.start();
//main线程在开启新的线程之后,会继续执行main方法中的代码
//3.使用main线程往集合中添加1000个元素
System.out.println("main线程开启往集合中添加元素!");
for (int i = 0; i <1000 ; i++) {
MyThread.list.add(i);//[0,1,2,3....999]
Thread.sleep(1);//睡眠1毫秒目的,为了提高安全问题出现的几率
}
System.out.println("main线程执行for循环结束,睡眠3秒钟,等待Thread-0线程执行结束!");
Thread.sleep(3000);
System.out.println("两个线程都执行完毕,打印集合的长度:"+MyThread.list.size());
}
}
ArrayList集合并发的问题:
1.存储的元素个数不对
2.会引发索引越界异常
main线程开启往集合中添加元素!
Thread-0线程开始执行线程任务,往集合中添加元素!
main线程执行for循环结束,睡眠3秒钟,等待Thread-0线程执行结束!
Thread-0执行for循环结束,执行线程任务结束!
两个线程都执行完毕,打印集合的长度:1985
2.并发Set集合_CopyOnWriteArraySet(重点)
Set集合的特点:
1.不允许存储重复元素
2.不包含索引的方法
需求:
1.创建一个被多个线程共享使用静态的HashSet集合对象
2.使用Thread-0线程往集合中添加1000个元素
3.使用main线程往集合中添加1000个元素
4.统计集合的长度
package com.itheima.demo12Set;
import java.util.HashSet;
import java.util.concurrent.CopyOnWriteArraySet;
/*
Set集合的特点:
1.不允许存储重复元素
2.不包含带索引的方法
*/
public class MyThread extends Thread{
//1.创建一个被多个线程共享使用静态的HashSet集合对象
/*
HashSet集合是一个多线程不安全的集合:
多线程一起添加数据:1.会丢失一些数据
*/
//public static HashSet<Integer> set =new HashSet<>();
/*
java.util.concurrent.CopyOnWriteArraySet<E>:是JDK1.5之后出现了
CopyOnWriteArraySet底层采用了CAS机制,可以保证多线程的安全
CAS机制是乐观锁,效率高
*/
public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
@Override
public void run() {
//2.使用Thread-0线程往集合中添加1000个元素
System.out.println("Thread-0线程开始执行线程任务,往集合中添加元素!");
for (int i = 0; i < 1000; i++) {
set.add(i);//[0,1,2,3....999]
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-0执行for循环结束,执行线程任务结束!");
}
}
package com.itheima.demo12Set;
public class Demo01Set {
public static void main(String[] args) throws InterruptedException {
//创建MyThread对象,调用start方法,开启一个新的线程,执行run方法
MyThread mt = new MyThread();
mt.start();
//main线程在开启新的线程之后,会继续执行main方法中的代码
//3.使用main线程往集合中添加1000个元素
System.out.println("main线程开启往集合中添加元素!");
for (int i = 1000; i <2000 ; i++) {
MyThread.set.add(i);//[1000,1001,1002,1003....1999]
Thread.sleep(1);//睡眠1毫秒目的,为了提高安全问题出现的几率
}
System.out.println("main线程执行for循环结束,睡眠3秒钟,等待Thread-0线程执行结束!");
Thread.sleep(3000);
System.out.println("两个线程都执行完毕,打印集合的长度:"+ MyThread.set.size());
}
}
HashSet集合存在并发问题:
main线程开启往集合中添加元素!
Thread-0线程开始执行线程任务,往集合中添加元素!
Thread-0执行for循环结束,执行线程任务结束!
main线程执行for循环结束,睡眠3秒钟,等待Thread-0线程执行结束!
两个线程都执行完毕,打印集合的长度:1988
3.并发Map集合_ConcurrentHashMap(重点)
需求:
1.创建一个被多个线程共享使用静态的HashMap集合对象
2.使用Thread-0线程往集合中添加1000个元素
3.使用main线程往集合中添加1000个元素
4.统计集合的长度
package com.itheima.demo13Map;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
public class MyThread extends Thread{
//1.创建一个被多个线程共享使用静态的HashMap集合对象
/*
HashMap集合是一个多线程不安全的集合:
多线程一起添加数据:1.会丢失一些数据 2.会有索引越界异常
*/
//public static HashMap<Integer,Integer> map =new HashMap<>();
/*
java.util.Hashtable<K,V>:JDK1.0时期的集合(最早期的双列集合)
不像新的 collection 实现,Hashtable 是同步的
Hashtable集合底层采用了synchronized同步机制,可以保证多线程的安全
synchronized是悲观锁,效率低下
*/
//public static Hashtable<Integer,Integer> map = new Hashtable<>();
/*
java.util.concurrent.ConcurrentHashMap<K,V><E>:是JDK1.5之后出现的
ConcurrentHashMap底层采用了CAS机制,可以保证多线程的安全
CAS机制是乐观锁,效率高
*/
public static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
@Override
public void run() {
//2.使用Thread-0线程往集合中添加1000个元素
System.out.println("Thread-0线程开始执行线程任务,往集合中添加元素!");
for (int i = 0; i < 1000; i++) {
map.put(i,i);//[0-0,1-1,2-2,3-3....999-999]
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-0执行for循环结束,执行线程任务结束!");
}
}
package com.itheima.demo13Map;
public class Demo01Map {
public static void main(String[] args) throws InterruptedException {
//创建MyThread对象,调用start方法,开启一个新的线程,执行run方法
MyThread mt = new MyThread();
mt.start();
//main线程在开启新的线程之后,会继续执行main方法中的代码
//3.使用main线程往集合中添加1000个元素
System.out.println("main线程开启往集合中添加元素!");
for (int i = 1000; i <2000 ; i++) {
MyThread.map.put(i,i);//[1000-1000,1001-1001,...1999-1999]
Thread.sleep(1);//睡眠1毫秒目的,为了提高安全问题出现的几率
}
System.out.println("main线程执行for循环结束,睡眠3秒钟,等待Thread-0线程执行结束!");
Thread.sleep(3000);
System.out.println("两个线程都执行完毕,打印集合的长度:"+ MyThread.map.size());
}
}
hashMap集合存在并发问题:
main线程开启往集合中添加元素!
Thread-0线程开始执行线程任务,往集合中添加元素!
Thread-0执行for循环结束,执行线程任务结束!
main线程执行for循环结束,睡眠3秒钟,等待Thread-0线程执行结束!
两个线程都执行完毕,打印集合的长度:1993
4.比较ConcurrentHashMap和Hashtable的效率
Java类库中,从1.0版本也提供一个线程安全的Map:Hashtable
Hashtable和ConcurrentHashMap有什么区别:
Hashtable采用的synchronized——悲观锁,效率更低。
ConcurrentHashMap:采用的CAS 机制——乐观锁,效率更高。
需求:
1.创建一个被多个线程共享使用静态的Hashtable集合(ConcurrentHashMap集合)对象
2.开启1000个线程,每个线程往集合中存储100000个数据
package com.itheima.demo14Map;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
/*
Hashtable和ConcurrentHashMap有什么区别:
Hashtable采用的synchronized——悲观锁,效率更低。
ConcurrentHashMap:采用的CAS 机制——乐观锁,效率更高。
*/
public class MyThread extends Thread {
//1.创建一个被多个线程共享使用静态的Hashtable集合(ConcurrentHashMap集合)对象
//public static Hashtable<Integer,Integer> map = new Hashtable<>();
public static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
@Override
public void run() {
//每个线程往集合中存储100000个数据
long s = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
map.put(i,i);
}
long e = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"往集合中添加10w个数据耗时:"+(e-s)+"毫秒!");
}
}
package com.itheima.demo14Map;
public class Demo01Map {
public static void main(String[] args) {
//开启1000个线程
for (int i = 0; i < 1000; i++) {
new MyThread().start();
}
}
}
Hashtable效率低下原因:
public synchronized V put(K key, V value)
public synchronized V get(Object key)
Hashtable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法,其他线程也访问Hashtable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。