1.概述
当多个线程在同一时刻访问同一种共享资源时,可能会造成数据的不一致等问题;
为了避免该问题的发生,就需要对线程之间进行协调和通信,而线程之间的协调和通信就是线程同步机制。
一个多线程的程序 如果是通过Runnable 接口来 实现的,则意味着 类中的属性将被多个线程共享,这样一来也会引发一些安全问题。
例如,统计车间工人的数量,经常有人来回出入,很难统计正确。
为解决这样的问题,就需要实现多线程同步,限制某个资源在某一时刻只能被一个线程访问。
同步 指的是多个操作在同一时间段内,只能有一个线程进行。其他线程要等待此线程完成之后才可以继续执行。
2.实现方式(两种)
只需要将线程的 并行 改为 串行 就可以解决该问题。
此时可以解决问题,但是效率相对比较低,因此建议能不用则不用。
为了协调多个线程的执行,使用java中的 synchronized 关键字,来保证原子性。实现同步锁/对象锁。
Java的内部提供了一种 锁机制 来保证 原子性。
原子性:把整个方法的锁定的代码,全部执行完毕。 下一个线程才能执行。
(1)同步方法: 使用关键字直接修饰整个方法,锁定整个方法的代码块。
权限修饰符 synchronized 返回值类型 方法名([参数1,...]){ 需要同步的代码; }
(2)同步代码块: 用该关键字修饰的代码块 被称为 同步代码块。
synchronized(同步对象){ 需要同步的代码; }
或者:
synchronized(this){ //注意: 同步方法的锁是当前调用该方法的对象,也就是this指向的对象。 希望被锁定的代码块; }
原理分析(尽量理解)
当多个线程同时执行同一个方法时,为了避免线程之间的冲突问题,通常都会给该方法加上一把同步锁;
当有线程先抢到同步锁时就可以进入同步语句块执行,其他线程只能进入阻塞状态;
当该线程执行完毕同步语句块后会自动释放同步锁,阻塞的线程又可以抢占同步锁,抢占成功的线程去执行同步语句块,抢占不成功的线程继续阻塞。
案例:解决电影院售票产生的 线程安全问题:
问题代码:
public class Ticket implements Runnable { //实现Runnable接口 创建多线程 引发 线程安全问题
private int tickets=100;
public void run() {
while(true) {
try {
if(tickets>0) {
Thread.sleep(10); //使线程休眠
String name=Thread.currentThread().getName(); //获取当前线程的名称
System.out.println(name+"正在发售第"+ tickets-- +"张票");
}else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类
public static void main(String[] args) {
Ticket ticket= new Ticket(); //创建线程任务
Thread t1 = new Thread(ticket,"窗口1"); // 定义4个线程对象,代表4个售票窗口
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
Thread t4 = new Thread(ticket,"窗口4");
t1.start(); //开启四个线程
t2.start();
t3.start();
t4.start();
}
结果:
从运行结果看,不合逻辑 出现负数 ...
解决方式一: 使用同步代码块
public class Ticket implements Runnable { //解决线程安全问题,使用 同步代码块(同步锁)
private int tickets=100;
public void run() {
while(true) {
synchronized(this) { //将this设置为 锁对象
try {
if(tickets>0) {
Thread.sleep(10); //使线程休眠
String name=Thread.currentThread().getName(); //获取当前线程的名称
System.out.println(name+"正在发售第"+ tickets-- +"张票");
}else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
测试代码:
public static void main(String[] args) {
Ticket ticket= new Ticket();//创建线程任务
// 定义4个线程对象,代表4个售票窗口
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
Thread t4 = new Thread(ticket,"窗口4");
//开启四个线程
t1.start();
t2.start();
t3.start();
t4.start();
}
问题得到解决:
方式二:使用同步方法
public class Ticket implements Runnable { //解决线程安全问题 使用 同步方法
private int tickets=100;
public void run() {
while(true) {
saleTicket(); //调用售票方法
if(tickets<=0) {
break;
}
}
}
private synchronized void saleTicket() { //定义一个同步方法 saleTicket()
if(tickets>0) {
try {
if(tickets>0) {
Thread.sleep(10); //使线程休眠10ms
String name=Thread.currentThread().getName(); //获取当前线程的名称
System.out.println(name+"正在发售第"+ tickets-- +"张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类:
public static void main(String[] args) {
Ticket ticket= new Ticket(); //创建线程任务
Thread t1 = new Thread(ticket,"窗口1"); // 定义4个线程对象,代表4个售票窗口
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
Thread t4 = new Thread(ticket,"窗口4");
t1.start(); //开启四个线程
t2.start();
t3.start();
t4.start();
}
复习:
StringBuilder类 - 后期增加的类,非线程安全的类,效率比较高 【不支持线程的同步技术。】
StringBuffer类 - 早期就有的类,线程安全的类, 效率比较低。 【 支持线程的同步技术。】
3.怎么加锁?
只需要在 重写的run()方法返回值类型前面 加个 关键字synchronized 同步锁,表示将整个方法的所有代码 全部锁定。
当方法体中的代码 全部执行完毕,同步锁会自动打开。
修改后的代码:(即添加了同步锁后)
package com.monkey1030; public class Account implements Runnable { private int money = 1000; //账户余额 @Override public synchronized void run() { // synchronized !!!!! // 模拟ATM机器 去后台服务器读取账户余额的过程 int temp = money; // 1000 // 判断账户余额是否 >=200 if(temp >= 200) { // 模拟取款过程 System.out.println("正在出钞,请稍后...."); temp -= 200; // 账户余额扣200: 1000 - 200 = 800 try { Thread.sleep(3000); // 休眠3秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); }else { System.out.println("账户余额不足,请重新核对金额!"); } money = temp; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } }
测试类:
package com.monkey1030; public class Test { public static void main(String[] args) { Account acc=new Account(); Thread t1=new Thread(acc); Thread t2=new Thread(acc); t1.start(); t2.start(); //让主线程 等待子线程终止,然后打印账户余额 try { t1.join(); t2.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("账户余额是: "+acc.getMoney()); } }
结果:
正在出钞,请稍后.... 请取走您的钞票! 正在出钞,请稍后.... 请取走您的钞票! 账户余额是: 600
注意:
当需要一个静态方法加锁时: “( )”括号中 不再是this
pubic synchronized static void xxx(类名.class){ ............... }
该方法 锁的是对象是 类对象
STRINGBUFFER 是同步的 SYNCHRONIZED APPEND();
• STRINGBUILDER 不是同步的 APPEND();
• VECTOR 和 HASHTABLE 是同步的
• ARRAYLIST 和 HASHMAP 不是同步的
• COLLECTIONS.SYNCHRONIZEDLIST()
• COLLECTIONS.SYNCHRONIZEDMAP()
• ARRAYLIST LIST = NEW ARRAYLIST();
• LIST SYNCLIST = COLLECTIONS.SYNCHRONIZEDLIST(LIST);
主要取决于 类中的方法 有无Synchronized 关键字。
二,死锁的发生
1.概述:
当两个线程或线程者多个互相锁定时就形成了死锁。【使用Synchronized关键字 加锁的时候,要避免死锁的发生。】
2. 避免死锁的原则:
• 顺序上锁,反向解锁,不要回头。
注意:
切记尽量不要使用同步代码块的嵌套!
• JAVA线程的API中很多过时方法都构成了死锁,因此不能调用。
如:
线程一执行的代码:
public void run(){ synchronized(a){ - 该线程持有对象锁a,等待对象锁b synachronized(b){ ... ... } } }
线程二执行的代码:
public void run(){ synchronized(b){ - 该线程持有对象锁b,等待对象锁a synachronized(a){ ... ... } } }