线程同步机制
线程同步是指多个线程操作同一个资源
并发是指同一个对象被多个线程同时操作
现实生活中 , 我们会遇到“ 同一个资源 , 多个人都想使用 ” 的问题 , 比如 , 食堂排队打饭, 每个人都想吃饭, 最天然的解决办法就是 排队, 一个个来 处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 . 这时候我们就需要线程同步。 线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个 对象的等待池 形成队列 , 等待前面线程使用完毕 , 下一个线程再使用 由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时 , 也带来了访问冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入 锁机制 synchronized , 当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 , 使用后释放锁即可。 存在以下问题 : 1,一个线程持有锁会导致其他所有需要此锁的线程挂起 ; 2,在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换和调度延时 , 引起性能问题 ; 3,如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置 , 引起性能问题。三大不安全案例
案例一:抢票
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String args[]){
BuyTicket station = new BuyTicket();
new Thread(station,"学生").start();
new Thread(station,"教师").start();
new Thread(station,"黄牛").start();
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
boolean flag = true; //外部停止方式
public void run(){
//买票
while(flag){
try{
buy();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException{
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
}
}
也可能得到-1,那是因为:
每个线程在自己的工作内存交互,内存控制不当会导致数据不一致
通俗解释就是可能多个人都同时抢一张票,但是票只有一张,所以会出现0和-1的情况
案例二:银行取钱
//不安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String agrs[]){
//账户
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account,50,"你");
Drawing girlFriend = new Drawing(account,100,"girlFriend");
you.start();
girlFriend.start();
}
}
//账户
class Account{
int money; //余额
String name; //卡名
public Account(int money,String name)
{
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account; //账户
//取了多少钱
int drawingMoney;
//现在手里有多少钱
int nowMoney;
public Drawing(Account account,int drawingMoney,String name){
super(name); //相当于父类构造方法(name),Drawing继承了Thread,Thread类中有一个构造方法是String name的
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
public void run(){
//判断有没有钱
if (account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
//模拟个延时看看(不是一定要有,这里只是为了找出错误)
//sleep可以放大问题的发生性
//因为真实的代码肯定不是你一个人用,是成千上百万的人一起用
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
//卡内余额 = 余额-你取得钱
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
//Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"手里的钱:"+nowMoney);
}
}
案例三:线程
import java.util.ArrayList;
import java.util.List;
public class UnsafeList {
public static void main(String agrs[]){
List<String> list = new ArrayList<String>();
for (int i=0;i<10000;i++){
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(list.size());
}
}
到不了10000是因为有一起走的情况。。。
同步方法及同步块
同步方法
1,由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 , 它包括两种用法 : synchronized方法 和 synchronized块同步方法:public synchronized void method(int args){}
2,synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个 synchronized方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞 , 方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行
缺陷:若将一个大的方法声明为synchronized将会影响效率
同步方法弊端
方法里面需要修改的内容才需要锁, 锁的太多 , 浪费资源
实践:
对三大不安全案例的修改:
案例一:
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String args[]){
BuyTicket station = new BuyTicket();
new Thread(station,"学生").start();
new Thread(station,"教师").start();
new Thread(station,"黄牛").start();
}
}
class BuyTicket implements Runnable {
//票
private int ticketNums = 10;
boolean flag = true; //外部停止方式
public void run() {
//买票
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//synchronized同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
}
}
发现都是一个人拿了所有票,这是因为sleep在buy中,sleep不能解开锁,而synchronized偏向于让优先级大的先行,其他的没有机会了。
修改:
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String args[]){
BuyTicket station = new BuyTicket();
new Thread(station,"学生").start();
new Thread(station,"教师").start();
new Thread(station,"黄牛").start();
}
}
class BuyTicket implements Runnable {
//票
private int ticketNums = 10;
boolean flag = true; //外部停止方式
public void run() {
//买票
while (flag) {
try {
//模拟延时,避免一个人拿到所有票sleep要写在这,因为sleep不会解开锁
Thread.sleep(100);
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//synchronized同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
//买票
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
}
}
因为buy方法被锁了,所以一次只有一个线程能调用它
案例二:
假如也用案例一的方法进行修改:
//不安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String agrs[]){
//账户
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account,50,"你");
Drawing girlFriend = new Drawing(account,100,"girlFriend");
you.start();
girlFriend.start();
}
}
//账户
class Account{
int money; //余额
String name; //卡名
public Account(int money,String name)
{
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account; //账户
//取了多少钱
int drawingMoney;
//现在手里有多少钱
int nowMoney;
public Drawing(Account account,int drawingMoney,String name){
super(name); //相当于父类构造方法(name),Drawing继承了Thread,Thread类中有一个构造方法是String name的
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
public synchronized void run(){ //案例一的修改方式
//判断有没有钱
if (account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
//模拟个延时看看(不是一定要有,这里只是为了找出错误)
//sleep可以放大问题的发生性
//因为真实的代码肯定不是你一个人用,是成千上百万的人一起用
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
//卡内余额 = 余额-你取得钱
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
//Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"手里的钱:"+nowMoney);
}
}
结果仍然错误:
这是因为synchronized默认锁的是this,这里要锁的是银行。
即:这里说的锁银行而不是锁this 指的是当前类 也就是银行的class Drawing.class
而synchronized方法 想让锁对象是当前类的class ,要么只能有一个银行的实例对象, 要么ynchronized方法前用static修饰 这样也能保证锁对象是类的class
而他有两个银行的实例对象 you 和girlfriend 两个实例对象 那么这两个实例对象开启了两条线程,每条线程用的锁对象都是当前实例对象 锁对象不同是无法实现同步的。
最简单的方法就是 用synchronized代码块 在synchronized(){}, 小括号中放当前类的class,因为一个类是只能有一个class的 所以 能保证锁对象是一样的 而实现同步
ps:小括号中只要是一个唯一的就行 对象 class都可以 其实只要记住下面这三点 任何时候都不会搞错同步方法或同步代码块中的锁对象是什么了
对于普通同步方法,锁是当前实例对象。 如果有多个实例 那么锁对象必然不同无法实现同步。
对于静态同步方法,锁是当前类的Class对象。有多个实例 但是锁对象是相同的 可以完成同步。
对于同步方法块,锁是Synchonized括号里配置的对象。对象最好是只有一个的 如当前类的 class 是只有一个的 锁对象相同 也能实现同步。
正确写法:
同步块
1,同步块:synchronized (Obj ) { }
2,Obj 称之为 同步监视器
(1)Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器
(2)同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是
这个对象本身 , 或者是 class [ 反射中讲解 ] 3,同步监视器的执行过程 (1)第一个线程访问 , 锁定同步监视器 , 执行其中代码 . (2)第二个线程访问 , 发现同步监视器被锁定 , 无法访问 . (3)第一个线程访问完毕 , 解锁同步监视器 . (4)第二个线程访问 , 发现同步监视器没有锁 , 然后锁定并访问 修改后://不安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String agrs[]){
//账户
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account,50,"你");
Drawing girlFriend = new Drawing(account,100,"girlFriend");
you.start();
girlFriend.start();
}
}
//账户
class Account{
int money; //余额
String name; //卡名
public Account(int money,String name)
{
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account; //账户
//取了多少钱
int drawingMoney;
//现在手里有多少钱
int nowMoney;
public Drawing(Account account,int drawingMoney,String name){
super(name); //相当于父类构造方法(name),Drawing继承了Thread,Thread类中有一个构造方法是String name的
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
public void run(){
//锁的对象就是变化的量,需要增删改的对象
synchronized (account){ //修改后
//判断有没有钱
if (account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
//模拟个延时看看(不是一定要有,这里只是为了找出错误)
//sleep可以放大问题的发生性
//因为真实的代码肯定不是你一个人用,是成千上百万的人一起用
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
//卡内余额 = 余额-你取得钱
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
//Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"手里的钱:"+nowMoney);
}
}
}
案例三:
import java.util.ArrayList;
import java.util.List;
public class UnsafeList {
public static void main(String agrs[]){
List<String> list = new ArrayList<String>();
for (int i=0;i<10000;i++){
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(list.size());
}
}