java基础之多线程三:多线程并发同步
由于线程的执行是CPU随机调度的,比如我们开启10个线程,这10个线程并不是同时执行的,而是CPU快速的在这10个线程之间切换执行,由于切换速度极快使我们感觉同时执行罢了。
线程同步问题往往发生在多个线程调用同一方法或者操作同一变量,但是我们要知道其本质就是CPU对线程的随机调度,CPU无法保证一个线程执行完其逻辑才去调用另一个线程执行。
比如:
4个窗口同时售卖100张车票:
public static void main(String[] args) { //测试: 卖票的动作. //1. 因为是四个窗口, 所以需要创建四个线程对象. 给线程自定义名字 MyThread mt1 = new MyThread("窗口1"); MyThread mt2 = new MyThread("窗口2"); MyThread mt3 = new MyThread("窗口3"); MyThread mt4 = new MyThread("窗口4"); //2. 开启线程 mt1.start(); mt2.start(); mt3.start(); mt4.start(); } public class MyThread extends Thread{ //需求: 四个窗口, 卖100张票. /* * 思路: * 1. 定义一个变量(tickets), 记录票数. * 2. 因为是四个窗口 同时 卖票, 通过多线程卖票. */ //1. 定义一个变量(tickets), 记录票数. private static int tickets = 100; //因为是共享数据, 所以用static修饰 //3. 使用父类的构造方法 public MyThread() { super(); } public MyThread(String name) { super(name); } //2. 因为是四个窗口 同时 卖票, 通过多线程卖票. @Override public void run() { /* * 卖票的动作 的 思路: * A: 因为不知道还有多少张票要买, 所以用while(true). * B: 做一下越界处理. 没票就不卖了. * C: 如果有票, 就正常的卖票即可. */ //A while(true) { //B if (tickets < 1) { break; } //为了加大出现错误票的概率, 我们加入: 休眠线程的概念 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } //C // 线程2休眠, 线程3休眠, 线程4休眠, System.out.println(getName() + "正在出售第" + tickets-- + "张票"); /* * 出现负数票的原因: if()判断, 休眠线程 * 假设现在卖到最后一张票了, tickets的值应该是1, 此时, * 如果线程1抢到了资源, 会越过if(), 然后休眠, 以此类推, 四个线程都处于休眠的状态. * * 休眠时间过了之后, 程序继续运行. * 假设线程1先抢到资源, 打印: 出售1号票, 然后会把tickets的值改为: 0 * 假设线程2后抢到资源, 打印: 出售0号票, 然后会把tickets的值改为: -1 * 假设线程3后抢到资源, 打印: 出售-1号票, 然后会把tickets的值改为: -2 * 假设线程4后抢到资源, 打印: 出售-2号票, 然后会把tickets的值改为: -3 */ /* * 出现重复值的原因: tickets-- * tickets-- 相当于 tickets = tickets - 1 * tickets-- 做了 3件事: * A: 读值. 读取tickets的值. * B: 改值. 将tickets的值 - 1. * C: 赋值. 将修改后的值重新赋值给 tickets. * 还没有来得及执行 C的动作, 此时别的线程抢走资源了, 就会出现重复值. * */ } } }
运行结果会出现下面这种一张票重复售卖或者出现卖负数票的情况:
窗口2正在出售第99张票
窗口1正在出售第98张票
窗口3正在出售第99张票
窗口3正在出售第95张票
窗口3正在出售第94张票
窗口3正在出售第93张票
所以我们可以说这种多个线程同时并发操作同一数据的时候会出现错误,是有安全问题的。
解决方案:
可使用同步代码块或同步方法来解决(synchronized )
//A while(true) { //while循环中的代码就是一次完成的卖票过程, 之所以会出现非法值的票, //原因是因为: 某个线程在卖票期间, 被别的线程抢走CPU资源了. //其实解决方案很简单: 在某一个线程卖(一次)票期间, 别的线程不能干预. synchronized (MyThread.class) { //要加锁的代码 //B if (tickets < 1) { break; } //为了加大出现错误票的概率, 我们加入: 休眠线程的概念 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } //C // 线程2休眠, 线程3休眠, 线程4休眠, System.out.println(getName() + "正在出售第" + tickets-- + "张票"); } }
综上所述,我们发现如果多个线程同时操作同一数据的情况,使用同步代码块或方法同步就可以解决了。
那么我们下面说一下同步代码块和同步方法的锁对象:
非静态方法同步对应的锁是this
public class Demo {
//同步代码块 public void method1() { synchronized (this) { System.out.print("山"); System.out.print("东"); System.out.print("张"); System.out.print("学"); System.out.print("友"); System.out.print("\r\n"); } }
//同步方法
public synchronized void method2() {
System.out.print("s");
System.out.print("d");
System.out.print("z");
System.out.print("x");
System.out.print("y");
System.out.print("\r\n");
}
}
静态方法同步对应的锁是当前类的字节码文件对象:
public class Demo {
//静态方法代码块同步 public static void method1() { synchronized (Demo.class) { System.out.print("山"); System.out.print("东"); System.out.print("张"); System.out.print("学"); System.out.print("友"); System.out.print("\r\n"); } }
//同步静态方法
public static synchronized void method2() {
System.out.print("s");
System.out.print("d");
System.out.print("z");
System.out.print("x");
System.out.print("y");
System.out.print("\r\n");
}
}
这里说明一下:
/* * 静态同步方法 和 非静态同步方法的锁对象 * 静态方法: 锁对象: 该类的字节码文件对象. 也就是上面的Demo.class 非静态方法: 锁对象: this */
总结:普通同步方法的锁对象是this,静态同步方法的锁对象是类的字节码文件对象
当前类实例对象,同步代码块锁可以自己定义,只要保证操作同一数据的线程使用的是同一把锁即可。