前言
首先整理了一下在java中 块
的概念,然后整理了一下 线程安全
与 不安全
的示例,为了理解线程不安全,进而学习了 java内存模型
引出的 内存可见性
问题。
1. 什么是块,分为几种
花括号写上代码,一般称为块
加上 synchronized
的就为同步块。
java中 块有四种:
- 方法里面的块:
局部快
,
作用: 解决变量作用域 - 类中块,和属性同级,方法外,:
构造块
,
作用:初始化对象信息的。 - 在类中块上加个static ,称为
静态块
同步块
,称为同步监视器。
在方法里面,解决我们线程安全的问题,加上 synchronized 。
2. 静态块与构造块的区别
- 静态块加载一次,初始化类的。
- 构造块是初始化对象的。
- 静态块先于构造块执行。
一、 举例说明:并发情况下,线程不安全
先不说线程同步,可能这个概念不太好整。先说一下并发,因为线程同步的前提是 先有并发。
- 并发: 同一个
对象或者资源
被多个线程同时操作。 - 线程同步: 当多个线程同时操作一个对象 或者资源的时候,就会出现问题,这个问题是jvm调度无法避免的问题,这个问题就是线程不安全。要避免就要借助其他工具,而这个工具,再Java中叫做线程同步。用示例来表现一下线程不安全的故事:
1. 示例1:unsafe12306取票
需求:模仿12306取票,有10张票,3个人去取,判断 当票数小于0时打印出 票已卖完。
package com.feng.syn;
/**
* 线程不安全: 数据有负数、相同 ,这两种情况
* 原因:
* 1、负数,处于临界值1时,进来到了test()方法中,都休息了,先后醒来之后,有人拿了1,有人拿了0,有人拿了 -1,
* 2、相同,每个线程都有各自的工作空间,最开始时,每个工作空间都去 主存中复制数据,处理之后,在放到主存上去,但是处理的数据不快,
* 当其中一个线程拿了10这个数据后,处理完后还未放到主存上时,第二个线程就拿到了这个10.所以会出现两个10的情况。
*/
public class ch01_UnsafeTicketTest01 {
public static void main(String[] args) {
//一份资源
UnsafeWeb12306 web =new UnsafeWeb12306();
System.out.println(Thread.currentThread().getName());
//多个代理
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码蟥").start();
}
}
class UnsafeWeb12306 implements Runnable{
//票数
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
test();
}
}
public void test(){
if(ticketNums<0) {
flag = false;
System.out.println("票已卖完");
return;
}
//模拟延时
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
结果却发现,有重复值,有负数,相当于判断没起作用。这就是线程不安全。原因就是:3个进程同时操作这个唯一的资源,就会出现线程不安全的情况。
2. 示例2:unsafe银行取钱
需求:银行里有100块钱结婚礼金,两个人去取,打印出账户的余额和取出放在口袋里的钱。
银行信息bean
Account.java 类
package com.feng.syn;
public class Account {
int money; //金额
String name; //名称
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
ch02_UnsafeMoneyTest02.java类
package com.feng.syn;
/**
* 线程不安全: 取钱
*
* 逻辑:
* 连取两次 则为负数 -70;
* 各自的口袋的钱是没问题的。
*/
public class ch02_UnsafeMoneyTest02 {
public static void main(String[] args) {
//账户
Account account =new Account(100,"结婚礼金");
Drawing you = new Drawing(account,80,"可悲的你");
Drawing wife = new Drawing(account,90,"happy的她");
you.start();
wife.start();
}
}
//模拟 取款
class Drawing extends Thread{
Account account ; //取钱的账户
int drawingMoney ;//取的钱数
int packetTotal ; //口袋的总数
public Drawing(Account account, int drawingMoney,String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
try {
Thread.sleep(2000); // 如果不堵塞的话,速度太快。看不到问题
} catch (InterruptedException e) {
e.printStackTrace();
}
if(account.money -drawingMoney<0) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -=drawingMoney;
packetTotal +=drawingMoney;
System.out.println(this.getName()+"-->账户余额为:"+account.money);
System.out.println(this.getName()+"-->口袋的钱为:"+packetTotal);
}
}
余额竟然会出现-70,的情况,原因是 100现金,她取90,剩10,我取80,剩-70。也就相当于那个判断没起作用。这就是线程不安全。
那为什么会有线程不安全的这一说呢。
二、线程不安全解密:内存可见性
讲解线程不安全之前,先理解下 java内存模型,由此产生的内存可见性问题。
1. java内存模型
- Java的内存模型分为主内存和工作内存(线程的)。
- Java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(如果对象很大怎么办呢,比如10个MB?虚拟机肯定不会那么傻把对象直接拷贝进去啦,会拷贝对象的地址,以及需要用到的字段的值,哪怕值也很大,比如大的字符串,也会有相应的机制保证不会全拷贝,不然不是直接就爆掉了么)。
- 线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。有一个关键字是
volatile
, volatile可以保证任何情况下变量的可见性,是不是就是直接读主内存呢,事实上,volatile变量依然有工作内存的拷贝,但是它的操作顺序比较特殊,会每次都从主内存重新加载,所以你会看到每次volatile读取到的都是最新的值。 - 不同的线程也无法访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
这里说的Java的主内存,工作内存与Java堆,栈,方法区等并不是同一个层次的划分,这两者基本是没有关系的,如果两者一定要勉强对应起来,那从变量,主内存,工作内存的定义上看,主内存只要对应于Java堆中的对象实例数据部分,工作内存对应于虚拟机栈中的部分区域
。从更低层次上来说,主内存就是直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存区中,因为程序运行时主要访问读写的是工作内存。
2. 解释线程不安全
- 当多个线程操作同一个对象或者同一个资源时(比如上面的12306买票和取钱),每个线程会把资源从java主内存中 读取一份到自己的工作内存中,进行操作,操作后,在将结果写到java主内存
中。 - 但是,因为有多个线程在同时操作,就要多个工作内存去读和写,假如有a,b两个进程,都读取了一份资源到自己的工作内存中, b内存可能没被更新到主内存去。导致a线程或者其他内存 从主内存拷贝数据到自己的工作区时,拷贝的不是最新的数据。这就是
内存可见性问题
。从而导致线程不安全问题。
3. 线程安全与不安全
-
线程安全:指多个线程在执行同一段代码的时候采用
加锁机制
,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。 -
线程不安全:是指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是
脏数据
三、线程同步
1. 概念
然后在说解决线程不安全的那个工具,就是线程同步
。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改 这个对象。 这时候,我们就需要用到**“线程同步”**。 线程同步其实就 是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待 池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
2. Synchronized 块和方法
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,*这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。
1) 同步方法:safe12306买票
public synchronized void method(int args) {}
synchronized 方法控制对“成员变量|类变量”对象的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法 的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获 得该锁,重新进入可执行状态。
同步方法解决12306买票的需求:
package com.feng.syn;
/**
* 线程不安全: 数据有负数、相同 ,这两种情况
* 原因:
* 1、负数,处于临界值1时,进来到了test()方法中,都休息了,先后醒来之后,有人拿了1,有人拿了0,有人拿了 -1,
* 2、相同,每个线程都有各自的工作空间,最开始时,每个工作空间都去 主存中复制数据,处理之后,在放到主存上去,但是处理的数据不快,
* 当其中一个线程拿了10这个数据后,处理完后还未放到主存上时,第二个线程就拿到了这个10.所以会出现两个10的情况。
*
* 解决上述问题
* 线程安全:在并发时保证数据的正确性、效率尽可能高
* synchronized
* 1、同步方法
* 2、同步块
*/
public class ch04_SafeSynMethodTicketTest01 {
public static void main(String[] args) {
//一份资源
SafeWeb12306 web =new SafeWeb12306();
System.out.println(Thread.currentThread().getName());
//多个代理
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码蟥").start();
}
}
class SafeWeb12306 implements Runnable{
//票数
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
//模拟延时
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
test();
}
}
/**
* 线程安全,同步 成员方法,锁的就是 this。 锁住的是对象 this
*
* 锁了资源,资源是对象的资源。此时这个资源是 this
*/
public synchronized void test(){
if(ticketNums<0) {
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
3) 同步块:safe银行取钱
synchronized (obj){ }, // obj称之为同步监视器
- obj可以是任何对象,但是推荐使用共享资源作为同步监视 器
- 同步方法中无需指定同步监视器,因为**同步方法的同步监 视器是this即该对象本身,或class即类的模子**
- 同步监视器的执行过程 ::
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器未锁,锁定并访问
缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
同步块解决银行取钱的需求:
package com.feng.syn;
/**
* 线程安全: 在并发时保证数据的正确性、效率尽可能高
* synchronized
* 1、同步方法
* 2、同步块 ,目标更明确
*
*/
public class ch06_SafeSynBlockMoneyTest02_2 {
public static void main(String[] args) {
//账户
Account account =new Account(100,"结婚礼金");
SafeSynDrawing you = new SafeSynDrawing(account,80,"可悲的你");
SafeSynDrawing wife = new SafeSynDrawing(account,90,"happy的她");
you.start();
wife.start();
}
}
//模拟取款 线程安全
class SafeSynDrawing extends Thread{
Account account ; //取钱的账户
int drawingMoney ;//取的钱数
int packetTotal ; //口袋的总数
public SafeSynDrawing(Account account, int drawingMoney,String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
test() ;
}
//目标锁定account
public void test() {
/**
* 提高性能
*/
if(account.money<=0) {
return ;
}
//同步块
synchronized(account) {
if(account.money -drawingMoney<0) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -=drawingMoney;
packetTotal +=drawingMoney;
System.out.println(this.getName()+"-->账户余额为:"+account.money);
System.out.println(this.getName()+"-->口袋的钱为:"+packetTotal);
}
}
}
四、synchronized 的范围问题,优化效率
- 有时候同步方法 所锁住的范围太大,线程安全,影响效率。
- 有时候同步块 所锁住的范围太小,锁不住,线程不安全。
1. 同步块、同步方法:12306 取票,double checking
package com.feng.syn;
/**
* 线程安全: 在并发时保证数据的正确性、效率尽可能高
* synchronized
* 1、同步方法
* 2、同步块
*
* @author 裴新 QQ:3401997271
*
*/
public class ch08_StudySynBlockTicketTest01 {
public static void main(String[] args) {
//一份资源
SynWeb12306 web =new SynWeb12306();
//多个代理
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码蟥").start();;
}
}
class SynWeb12306 implements Runnable{
//票数
private int ticketNums =10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
test5();
}
}
//线程安全:尽可能锁定合理的范围(不是指代码 指数据的完整性),,性能最好。
//double checking
public void test5() {
if(ticketNums<=0) {//考虑的是没有票的情况
flag = false;
return ;
}
synchronized(this) {
if(ticketNums<=0) {//考虑最后的1张票
flag = false;
return ;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
//线程不安全 范围太小锁不住
public void test4() {
synchronized(this) {
if(ticketNums<=0) {
flag = false;
return ;
}
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
//线程不安全 ticketNums对象在变
public void test3() {
synchronized((Integer)ticketNums) {
if(ticketNums<=0) {
flag = false;
return ;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
//线程安全 同步块 范围 太大 -->效率低下 ,,同步块,指定锁住的内容,肯定是不变的资源。
public void test2() {
synchronized(this) {
if(ticketNums<=0) {
flag = false;
return ;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
//线程安全 同步方法 同步方法锁住的是对象资源, 对象资源看方法里的内容
public synchronized void test1() {
if(ticketNums<=0) {
flag = false;
return ;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
2. 同步块:快乐影院 ,买几张票
package com.feng.syn;
/**
* 快乐影院
*
* 多线程 和 进程同步
*/
/**
* 如果不锁住的话,会使得可用的位置一直不变
*/
public class ch09_HappyCinema01 {
public static void main(String[] args) {
Cinema cinema = new Cinema(2, "shangxuetang");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Customer(cinema,2),"老毛").start();
new Thread(new Customer(cinema,1),"老冯").start();
}
}
//顾客
class Customer implements Runnable{
private Cinema cinema;
private int seats;
public Customer(Cinema cinema, int seats) {
this.cinema = cinema;
this.seats = seats;
}
@Override
public void run() {
synchronized (cinema){
boolean b = cinema.bookTickets(seats);
if (b){
System.out.println("出票成功"+Thread.currentThread().getName()+"-<位置为:"+seats);
}else{
System.out.println("出票失败"+Thread.currentThread().getName()+"-<位置不够");
}
}
}
}
// 影院
class Cinema{
private int available; // 可用的位置
private String name; // 名称
public Cinema(int available, String name) {
this.available = available;
this.name = name;
}
// 购票
public boolean bookTickets(int seats){
System.out.println("可用的位置:" + available);
if (seats>available){
return false;
}else {
available -= seats;
return true;
}
}
}
3. 同步块:快乐影院:可选座位
package com.feng.syn;
/**
* 快乐影院
*
* 多线程 和 进程同步
*/
import java.util.ArrayList;
import java.util.List;
/**
* 如果不锁住的话,会使得可用的位置一直不变
*/
public class ch10_HappyCinema02 {
public static void main(String[] args) {
//可用位置
List available =new ArrayList<Integer>();
available.add(1);
available.add(2);
available.add(3);
available.add(6);
available.add(7);
//顾客需要的位置
List<Integer> seats1 =new ArrayList<Integer>();
seats1.add(1);
seats1.add(2);
List<Integer> seats2 =new ArrayList<Integer>();
seats2.add(3);
seats2.add(6);
FengCinema fengCinema = new FengCinema(available, "shangxuetang");
new Thread(new HappyCustomer(fengCinema,seats1),"老毛").start();
new Thread(new HappyCustomer(fengCinema,seats2),"老冯").start();
}
}
//顾客
class HappyCustomer implements Runnable{
private FengCinema fengCinema;
private List<Integer> seats;
public HappyCustomer(FengCinema fengCinema, List<Integer> seats) {
this.fengCinema = fengCinema;
this.seats = seats;
}
@Override
public void run() {
synchronized (fengCinema){
boolean b = fengCinema.bookTickets(seats);
if (b){
System.out.println("出票成功"+Thread.currentThread().getName()+"-<位置为:"+seats);
}else{
System.out.println("出票失败"+Thread.currentThread().getName()+"-<位置不够");
}
}
}
}
// 影院
class FengCinema{
private List<Integer> available; // 可用的位置
private String name; // 名称
public FengCinema(List<Integer> available, String name) {
this.available = available;
this.name = name;
}
// 购票
public boolean bookTickets(List<Integer> seats){
System.out.println("欢迎光临"+this.name+",当前可用位置为:"+available);
List<Integer> copy = new ArrayList<Integer>();
copy.addAll(available);
//相减
copy.removeAll(seats); // 会将相同的值 给删掉 ,remove方法参数为集合
//判断大小
if(available.size()-copy.size() !=seats.size()) {
return false;
}else {
//成功
available = copy;
return true;
}
}
}
4. 同步方法:
package com.feng.syn;
public class ch11_Happy12306 {
public static void main(String[] args) {
Web12306 c = new Web12306(4,"happy sxt");
new Passenger(c,"老高",2).start();
new Passenger(c,"老裴",1).start();
}
}
//顾客
class Passenger extends Thread{
int seats;
public Passenger(Runnable target,String name,int seats) {
super(target,name);
this.seats = seats;
}
}
//火车票网
class Web12306 implements Runnable{
int available; //可用的位置
String name; //名称
public Web12306(int available, String name) {
this.available = available;
this.name = name;
}
public void run() {
Passenger p = (Passenger)Thread.currentThread();
boolean flag = this.bookTickets(p.seats);
if(flag) {
System.out.println("出票成功"+Thread.currentThread().getName()+"-<位置为:"+p.seats);
}else {
System.out.println("出票失败"+Thread.currentThread().getName()+"-<位置不够");
}
}
//购票
public synchronized boolean bookTickets(int seats) {
System.out.println("可用位置为:"+available);
if(seats>available) {
return false;
}
available -=seats;
return true;
}
}
五、简单了解锁机制
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带 来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问 时加入**锁机制(synchronized),当一个线程获得对象的排它锁**,独占资源, 其他线程必须等待,使用后释放锁即可。存在以下问题:
- 一个线程持有锁会导致其它所有需要此锁的线程挂起;
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时, 引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级 倒置,引起性能问题。
死锁:多个线程各自占有一些共享资源,并且互相 等待其他线程占有的资源才能进行,而导致两个 或者多个线程都在等待对方释放资源,都停止执 行的情形。某一个同步块同时拥有“两个以上对 象的锁”时,就可能会发生“死锁”的问题
1. 死锁案例
package com.feng.syn;
/**
* 死锁: 过多的同步可能造成相互不释放资源
* 从而相互等待,一般发生于同步中持有多个对象的锁
*
* 避免: 不要在同一个代码块中,同时持有多个对象的锁
*
*/
public class ch13_DeadLock {
public static void main(String[] args) {
Markup g1 = new Markup(1,"张柏芝");
Markup g2 = new Markup(0,"王菲");
g1.start();
g2.start();
}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
//化妆
class Markup extends Thread{
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
//选择
int choice;
//名字
String girl;
public Markup(int choice,String girl) {
this.choice = choice;
this.girl = girl;
}
@Override
public void run() {
//化妆
markup();
}
//相互持有对方的对象锁-->可能造成死锁
private void markup() {
if(choice==0) {
synchronized(lipstick) { //获得口红的锁
System.out.println(this.girl+"涂口红");
//1秒后想拥有镜子的锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
synchronized(mirror) {
System.out.println(this.girl+"照镜子");
}*/
}
synchronized(mirror) {
System.out.println(this.girl+"照镜子");
}
}else {
synchronized(mirror) { //获得镜子的锁
System.out.println(this.girl+"照镜子");
//2秒后想拥有口红的锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
synchronized(lipstick) {
System.out.println(this.girl+"涂口红");
} */
}
synchronized(lipstick) {
System.out.println(this.girl+"涂口红");
}
}
}
}