一、概念
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。 比如:教室里,只有一台电脑,多个人都想使用。
天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,
下一个线程再使用。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决
这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,
这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。
▪ synchronized 方法
通过在方法声明中加入 synchronized关键字来声明,语法如下:
public synchronized void accessVal(int newVal);
synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的
/*** * synchronized [英语]同步的。 线程锁,保证数据准确 * 线程安全,在并发时要保证数据准确,效率尽可能高 * 1、在某个方法前面加锁,即同步方法 * 2、同步块,你用那块给那块加锁。 */package cn.sxt.thread;//使用同步方法public class Test_0409_Synchronized { public static void main(String[] args) { SafeWeb12306 web=new SafeWeb12306(); //start()表示web对象调用run方法去了,不知什么时候回来,主方法继续往下不等它.又来个线程也去调用run方法去了 new Thread(web,"1号黄牛").start();//3个线程相互独立 new Thread(web,"2号黄牛").start(); new Thread(web,"3号黄牛").start(); }}class SafeWeb12306 implements Runnable{ private int ticketNum=10; private boolean flag=true; public void run() { while (flag) { buyTicket1(); } } //synchronized同步方法锁定的是与对象web相关的资源,ticketNum和flag都是它的资源,但有些时候可能存在锁的对象不对,仍然达不到效果 public synchronized void buyTicket() { //加了同步锁的方法 if (ticketNum<=0) { /*1、没加锁之前为什么出现负数票,当只有临界值即最后余票1张(编号001),当线程B进来时发现1不满足余票为0的情形 * 线程B拿了最后一张票,遇到网络延时200毫秒,然后睡觉去啦,但是还没到最后一步即修改内存中票的余数,就是这个语句ticketNum-- * (Thread.currentThread().getName()+"->"+ticketNum--) 此时线程A也进来发现还有一张票(没修过票的余数),于是他也拿一个 * 去睡觉去啦,同理线程C也进来了,也满足情况,也拿一张睡觉去啦。等到线程B先醒来,拿着票去修改内存票数(ticketNum--), * 输出1,A和C分别醒来,看到内存中的ticketNum分别为1和0,然后修改票的余数,然后输出0和-1 * 2、出现票号相同的情况 * 每个线程都独立的工作空间,当线程A把一张票的信息(假设是8)复制到自己空间是遇到网络延时,还没等把减去一张的信息(7)覆盖回去 * 线程B此时又把原来票的余数为8的信息复制到自己的空间去了,所以出现2个8 * */ System.out.println("没有余票了!"); flag=false; return; } try { //模拟网络延时 200毫秒延时 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } //Thread.currentThread().getName(),当前线程的名字,谁运行就输出谁的名字 System.out.println(Thread.currentThread().getName()+"->"+ticketNum--+"号票"); } //在保证安全的情况下提高性能的方式 public void buyTicket1() { //加了同步锁的方法 if (ticketNum<=0) { //双重检测,考虑没有票的情形 flag=false;//在明确已经没票后,线程C执行到这里判断一下,没票就退出来,不用走到synchronized的排队队列中 return; //傻傻的等 } synchronized (this) { //锁this对象,有ticketNum和flag2个属性,单锁一个锁不住.synchronized (属性1,属性2)是错误写法 if (ticketNum<=0) { //在有票的情况下,考虑最后一张票的情形,当线程A拿到同步锁后(在此之后谁也进不来),拿完票后 //把ticktNum减1后变为0,线程A解锁后,线程B进来只用判断一下,已经没票了,直接退出,不用执行下边的延时操作 //提升性能 flag=false; return; } try { //模拟网络延时 200毫秒延时 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } //Thread.currentThread().getName(),当前线程的名字,谁运行就输出谁的名字 System.out.println(Thread.currentThread().getName()+"->"+(ticketNum--)+"号票"); } }}
▪ synchronized块
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。
synchronized 块:通过 synchronized关键字来声明synchronized 块,语法如下:
synchronized(syncObject) { //允许访问控制的代码 }
/*** * 使用同步块 * synchronized (obj) { },obj称为同步监视器,它可以是任何对象,推荐使用共享资源作为同步监视器 无需指定同步监视器,因为同步方法的同步监视器是this即该对象的本身或class即类的模子 * * 过程: * 1、线程A访问,锁定同步监视器,执行其中的代码 * 2、线程B访问,发现同步监视器被锁定,无法访问 * 3、线程A访问完,解锁同步监视器 * 4、线程B访问,发现同步监视器未锁,锁定并访问 */package cn.sxt.thread;public class Test_0409_Synchronized_Drawing { public static void main(String[] args) { Account account=new Account("结婚基金", 100); Drawing me=new Drawing(account, 70, "小李"); Drawing she=new Drawing(account, 30, "媳妇"); me.start(); she.start(); }}//银行账户class Account{ String name; int money; public Account(String name, int money) { super(); this.name = name; this.money = money; } }//取钱的过程class Drawing extends Thread{ Account account;//取钱的账户 int drawingMoney;//每一笔取得钱数 int pocketMoney;//可能取多笔,总的取钱数 public Drawing(Account account, int drawingMoney,String name) { //String name取钱的姓名 super(name); this.account = account; this.drawingMoney = drawingMoney; } public void run() { core(); } //目标锁定即锁定账户余额。synchronized块锁 public void core() { if (account.money<=0) { //提高性能,先看账户有没有钱,没有就直接跳出,当然不可能为0,避免cpu调度 System.out.println("账户没钱了,穷逼!"); return; } /*一直到执行完synchronized块的最后一句System.out.println(this.getName()+"-->口袋的钱:"+ pocketMoney); * 才会解锁 * */ //提出问题:有性能问题,即使账户没钱了,线程也需要等然后就进来判断一下,看看是不是没钱了 synchronized (account) { //加了锁之后,如果第2个线程进来发现account.money-drawingMoney<0就会跳出 //不会往下执行 if (account.money >0) { System.out.println("结婚基金的余额:"+account.money); }else { System.out.println("账户没钱了,结个毛婚!"); } if (account.money-drawingMoney<0) { return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.money=account.money-drawingMoney;//账户总额减去要取得钱数 pocketMoney=pocketMoney+drawingMoney;//每取一笔,口袋的钱加一笔 System.out.println(this.getName()+"-->取完后账户余额:"+ account.money); System.out.println(this.getName()+"-->现在口袋里的钱:"+ pocketMoney); } } }