学习笔记 02 --- Java 多线程
synchronized 关键字:
脏读:在多线程中,难免会出现多个线程对同一个对象的实例变量进行并发操作的情况,如果没有同步处理,那么就会造成脏读,最后的结果是不正确的。
synchronized 同步方法、synchronized 同步代码块:
(1)对其他 synchronized 同步方法或者 synchronized(this) 同步代码块呈阻塞状态。
(2)多线程同一时间只有一个线程可以执行 synchronized 同步方法中的代码。(注:多线程指的是拥有同一个对象锁的线程,当一个线程再执行的时候,其他线程必须等待)
1、当一个线程访问 "某对象" 的 "synchronized 方法" 或者 "synchronized 代码块" 时,其他线程对 "该对象" 的该 "synchronized 方法" 或者 "synchronized 代码块" 的访问将被阻塞。
- public class ThreadTest {
- public static void main(String[] args) {
- Thread1 t = new Thread1();
- Thread t1 = new Thread(t, "t1");
- Thread t2 = new Thread(t, "t2");
- t1.start();
- t2.start();
- }
- }
- public class Thread1 implements Runnable {
- public void run() {
- synchronized(this) {
- for (int i = 0; i < 3; i++) {
- try {
- Thread.sleep(1000);
- System.out.println(Thread.currentThread().getName() + "正在执行!" + i);
- } catch(InterruptedException e) { // TODO Auto-generated catch blocke.printStackTrace();}}}}}
运行结果:
- t2正在执行!0t2正在执行!1t2正在执行!2t1正在执行!0t1正在执行!1t1正在执行!2
结果说明:
从结果我们可以看出 t2 线程执行完,t1 线程才继续执行,因为代码中 run() 方法中存在 synchronize 代码块,而且 t1 和 t2 这个两个流程都是基于 Thread1 这个对象来创建的流程,所以 t1 和 t2 两个流程共享 Thread1 这个同步锁,所以当一个线程执行的时候,另一个线程只能等待直到" 运行线程 " 释放 Thread1 的同步锁之后才能执行。
注:我们从代码中看到 t1 和 t2 两个线程启动的先后顺序是 t1 先启动,t2 后启动,那为什么结果输出是 t2 在前,t1 在后呢?这就说明了线程执行的顺序和启动的顺序是没有关系的,cpu 先调用哪个线程是无序的。
我们把上面的代码稍作改动,再看一看运行结果怎样:
- public class ThreadTest {
- public static void main(String[] args) {
- Thread1 t1 = new Thread1("t1");
- Thread1 t2 = new Thread1("t2");
- t1.start();
- t2.start();
- }
- }
- public class Thread1 extends Thread {
- public Thread1(String name) { // TODO Auto-generated constructor stubsuper(name);}public void run(){synchronized (this) {for(int i=0;i<3;i++){try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+"正在执行!"+i);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}}
运行结果:
- t2正在执行!0t1正在执行!0t2正在执行!1t1正在执行!1t1正在执行!2t2正在执行!2
结果说明:
为什么两段程序稍作改动后运行结果又所不同呢?因为 synchronized(this) 里面的 this 指的是 "当前对象",既 synchronized(this) 所在类对应的当前对象,它的作用就是获取 "当前对象" 的同步锁。对于第二部分代码而言,t1 和 t2 两个线程是 new 的两个不同的 Thread1 对象,所以在 t1 和 t2 执行 synchronized(this) 代码块的时候,获取的是不同的对象的同步锁,所以执行的时候不会造成阻塞。第一部分代码,t1he t2 两个线程共同拥有一个 Thread1 对象,所以一个线程再执行的时候,另一个线程会造成阻塞。
2、当一个线程访问 "某对象" 的 "synchronized 方法" 或者 "synchronized 代码块" 时,其他线程仍然可以访问 "该对象" 的非同步代码块。
- public class ThreadTest {
- public static void main(String[] args) {
- final NumTest test = new NumTest();
- Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {
- test.syncMethod();
- }
- },
- "t1");
- Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {
- test.nosyncMethod();
- }
- },
- "t2");
- t1.start();
- t2.start();
- }
- }
- public class NumTest {
- public void syncMethod() {
- synchronized(this) {
- for (int i = 0; i < 3; i++) {
- try {
- Thread.sleep(1000);
- System.out.println(Thread.currentThread().getName() + "正在执行!" + i);
- } catch(InterruptedException e) { // TODO Auto-generated catch blocke.printStackTrace();}}}}public void nosyncMethod(){for(int i=0;i<3;i++){try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+"正在执行!"+i);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}
运行结果:
- t2正在执行!0t1正在执行!0t2正在执行!1t1正在执行!1t2正在执行!2t1正在执行!2
结果说明:
主线程新建了两个线程 t1 和 t2,t1 调用 test 的 syncMethod() 方法,该方法内含有同步代码块;t2 调用 test 的 nosyncMethod() 方法,该方法不是同步方法。当 t1 线程运行的时候,虽然可以根据同步代码块拿到 test 的同步锁,但是并不影响 t2 线程执行 test 的非同步方法,因为 t2 不需要用到 test 的同步锁。
3、当一个线程访问 "某对象" 的 "synchronized 方法" 或者 "synchronized 代码块" 时,其他线程对 "该对象" 的其他的 "synchronized 方法" 或者 "synchronized 代码块" 的访问将被阻塞。
- public class ThreadTest {
- public static void main(String[] args) {
- final NumTest test = new NumTest();
- Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {
- test.syncMethod();
- }
- },
- "t1");
- Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {
- test.nosyncMethod();
- }
- },
- "t2");
- t1.start();
- t2.start();
- }
- }
- public class NumTest {
- public void syncMethod() {
- synchronized(this) {
- for (int i = 0; i < 3; i++) {
- try {
- Thread.sleep(1000);
- System.out.println(Thread.currentThread().getName() + "正在执行!" + i);
- } catch(InterruptedException e) { // TODO Auto-generated catch blocke.printStackTrace();}}}}public void nosyncMethod() {synchronized (this) {for (int i = 0; i < 3; i++) {try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+ "正在执行!" + i);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}}
运行结果:
- t2正在执行!0t2正在执行!1t2正在执行!2t1正在执行!0t1正在执行!1t1正在执行!2
结果说明:
我们只是把 test 类里面的 nosyncMethod() 方法也用 synchronized 关键字修饰,运行结果就会明显不一样,是因为主线程新建的 t1 合同 t2 两个线程共同拥有 test 的同步锁,虽然调用的方法不一样,但是调用的方法都是同步的,所以当一个线程执行的时候,获取到了 test 的同步锁,另一个线程就必须等待" 运行线程 " 释放掉 test 同步锁才能继续执行。
死锁:
当一个线程永远的持有一个对象的同步锁时,其他线程都在尝试去获取这个锁时,那么他们将永远都处于阻塞状态。
上面既然说到了锁这个概念,那么我们就了解一些死锁,死锁是设计上的一个 bug,死锁问题不是说即时 bug,程序发生死锁不代表程序每次都会发生死锁,但是当高并发、高负载的情况下,死锁发生的几率就会大大提升。当 Java 发生死锁时,除非终止并重启应用,否则两段同步代码或者代码块都无法运行。
死锁的例子如果线程 A 持有锁 L 并且想获得锁 M,线程 B 持有锁 M 并且想获得锁 L,那么这两个线程将永远等待下去。代码如下:
- public class DeadLock {
- private final Object left = new Object();
- private final Object right = new Object();
- public void leftRight() throws Exception {
- synchronized(left) {
- Thread.sleep(2000);
- synchronized(right) {
- System.out.println("leftRight end!");
- }
- }
- }
- public void rightLeft() throws Exception {
- synchronized(right) {
- Thread.sleep(2000);
- synchronized(left) {
- System.out.println("rightLeft end!");
- }
- }
- }
- }
- public class Thread0 extends Thread {
- private DeadLock dl;
- public Thread0(DeadLock dl) {
- this.dl = dl;
- }
- public void run() {
- try {
- dl.leftRight();
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- }
- public class Thread1 extends Thread {
- private DeadLock dl;
- public Thread1(DeadLock dl) {
- this.dl = dl;
- }
- public void run() {
- try {
- dl.rightLeft();
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- }
- public static void main(String[] args) {
- DeadLock dl = new DeadLock();
- Thread0 t0 = new Thread0(dl);
- Thread1 t1 = new Thread1(dl);
- t0.start();
- t1.start();
- }
避免死锁的方式:
1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。
2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。
3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然 synchronized 不具备这个功能,但是我们可以使用 Lock 类中的 tryLock 方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息。
通过 wait(),notifyAll() 实现生产者和消费者:
如果理解了 wait(),notifyAll(),synchronized() 的原理,那么生产者和消费者问题就很容易理解了,所以在这里 copy 网上一段生产者和消费者的代码,代码如下:
- // Demo1.java // 仓库 class Depot { private int capacity; // 仓库的容量 private int size; // 仓库的实际数量 public Depot(int capacity) { this.capacity = capacity; this.size = 0; } public synchronized void produce(int val) { try { // left 表示"想要生产的数量"(有可能生产量太多,需多此生产) int left = val; while (left > 0) { // 库存已满时,等待"消费者"消费产品。 while (size >= capacity) wait(); // 获取"实际生产的数量"(即库存中新增的数量) // 如果"库存"+"想要生产的数量">"总的容量",则"实际增量"="总的容量"-"当前容量"。(此时填满仓库) // 否则"实际增量"="想要生产的数量" int inc = (size+left)>capacity ? (capacity-size) : left; size += inc; left -= inc; System.out.printf("%s produce(=) --> left==, inc==, size==\n", Thread.currentThread().getName(), val, left, inc, size); // 通知"消费者"可以消费了。 notifyAll(); } } catch (InterruptedException e) { } } public synchronized void consume(int val) { try { // left 表示"客户要消费数量"(有可能消费量太大,库存不够,需多此消费) int left = val; while (left > 0) { // 库存为0时,等待"生产者"生产产品。 while (size <= 0) wait(); // 获取"实际消费的数量"(即库存中实际减少的数量) // 如果"库存"<"客户要消费的数量",则"实际消费量"="库存"; // 否则,"实际消费量"="客户要消费的数量"。 int dec = (size<left) ? size : left; size -= dec; left -= dec; System.out.printf("%s consume(=) <-- left==, dec==, size==\n", Thread.currentThread().getName(), val, left, dec, size); notifyAll(); } } catch (InterruptedException e) { } } public String toString() { return "capacity:"+capacity+", actual size:"+size; } } // 生产者 class Producer { private Depot depot; public Producer(Depot depot) { this.depot = depot; } // 消费产品:新建一个线程向仓库中生产产品。 public void produce(final int val) { new Thread() { public void run() { depot.produce(val); } }.start(); } } // 消费者 class Customer { private Depot depot; public Customer(Depot depot) { this.depot = depot; } // 消费产品:新建一个线程从仓库中消费产品。 public void consume(final int val) { new Thread() { public void run() { depot.consume(val); } }.start(); } } public class Demo1 { public static void main(String[] args) { Depot mDepot = new Depot(100); Producer mPro = new Producer(mDepot); Customer mCus = new Customer(mDepot); mPro.produce(60); mPro.produce(120); mCus.consume(90); mCus.consume(150); mPro.produce(110); } }
(01) Producer 是 "生产者" 类,它与 "仓库 (depot)" 关联。当调用 "生产者" 的 produce() 方法时,它会新建一个线程并向" 仓库 " 中生产产品。
(02) Customer 是 "消费者" 类,它与 "仓库 (depot)" 关联。当调用 "消费者" 的 consume() 方法时,它会新建一个线程并消费" 仓库 " 中的产品。
(03) Depot 是 "仓库" 类,仓库中记录 "仓库的容量 (capacity)" 以及 "仓库中当前产品数目 (size)"。
"仓库" 类的生产方法 produce() 和消费方法 consume() 方法都是 synchronized 方法,进入 synchronized 方法体,意味着这个线程获取到了该 "仓库" 对象的同步锁。这也就是说,同一时间,生产者和消费者线程只能有一个能运行。通过同步锁,实现了对 "残酷" 的互斥访问。
对于生产方法 produce() 而言:当仓库满时,生产者线程等待,需要等待消费者消费产品之后,生产线程才能生产;生产者线程生产完产品之后,会通过 notifyAll() 唤醒同步锁上的所有线程,包括 "消费者线程",即我们所说的 "通知消费者进行消费"。
对于消费方法 consume() 而言:当仓库为空时,消费者线程等待,需要等待生产者生产产品之后,消费者线程才能消费;消费者线程消费完产品之后,会通过 notifyAll() 唤醒同步锁上的所有线程,包括 "生产者线程",即我们所说的 "通知生产者进行生产"。
就爱阅读 www.92to.com 网友整理上传, 为您提供最全的知识大全, 期待您的分享,转载请注明出处。
来源: