Java多线程详解2

Java多线程详解

Java线程:线程的同步与锁

一、同步问题提出

线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。

例如:两个线程ThreadA、ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据。

publicclass Foo {
 privateint x = 100;

 publicint getX() {
 return x;
    } 

 publicint fix(int y) {
        x = x - y; 
 return x;
    } 
}
publicclass MyRunnableimplements Runnable {
 private Foo foo =new Foo(); 

 publicstaticvoid main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread ta = new Thread(r,"Thread-A"); 
        Thread tb = new Thread(r,"Thread-B"); 
        ta.start(); 
        tb.start(); 
    } 

 publicvoid run() {
 for (int i = 0; i < 3; i++) {
 this.fix(30);
 try {
                Thread.sleep(1); 
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            } 
            System.out.println(Thread.currentThread().getName() + " :当前foo对象的x值= " + foo.getX());
        } 
    } 

 publicint fix(int y) {
 return foo.fix(y);
    } 
}

运行结果:

Thread-A : 当前foo对象的x值= 40 Thread-B : 当前foo对象的x值= 40 Thread-B : 当前foo对象的x值= -20 Thread-A : 当前foo对象的x值= -50 Thread-A : 当前foo对象的x值= -80 Thread-B : 当前foo对象的x值= -80 Process finished with exit code 0

从结果发现,这样的输出值明显是不合理的。原因是两个线程不加控制的访问Foo对象并修改其数据所致。

如果要保持结果的合理性,只需要达到一个目的,就是将对Foo的访问加以限制,每次只能有一个线程在访问。这样就能保证Foo对象中数据的合理性了。

在具体的Java代码中需要完成一下两个操作:

把竞争访问的资源类Foo变量x标识为private;

同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。

二、同步和锁定

1、锁的原理

Java中每个对象都有一个内置锁

当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。

当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。

一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

释放锁是指持锁线程退出了synchronized同步方法或代码块。

关于锁和同步,有一下几个要点:

1)、只能同步方法,而不能同步变量和类;

2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?

3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。

4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。

6)、线程睡眠时,它所持的任何锁都不会释放。

7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。

8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。

9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:

    public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }

当然,同步方法也可以改写为非同步方法,但功能完全一样的,例如:

    public synchronized int getX() {
        return x++;
    }

    public int getX() {
        synchronized (this) {
            return x;
        }
    }

效果是完全一样的。

三、静态方法同步

要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。

例如:

public static synchronized int setName(String name){
      Xxx.name = name;
}
等价于
public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

四、如果线程不能不能获得锁会怎么样

如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的的一种池中,必须在哪里等待,直到其锁被释放,该线程再次变为可运行或运行为止。

当考虑阻塞时,一定要注意哪个对象正被用于锁定:

1、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。

2、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。

3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。

4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。

五、何时需要同步

在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。

对于非静态字段中可更改的数据,通常使用非静态方法访问。

对于静态字段中可更改的数据,通常使用静态方法访问。

如果需要在非静态方法中使用静态字段,或者在静态字段中调用非静态方法,问题将变得非常复杂。已经超出SJCP考试范围了。

六、线程安全类

当一个类已经很好的同步以保护它的数据时,这个类就称为“线程安全的”。

即使是线程安全类,也应该特别小心,因为操作的线程是间仍然不一定安全。

举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。

看个代码:

publicclass NameList {
 private List nameList = Collections.synchronizedList(new LinkedList());

 publicvoid add(String name) {
        nameList.add(name); 
    } 

 public String removeFirst() {
 if (nameList.size() > 0) {
 return (String) nameList.remove(0);
        } else {
 returnnull;
        } 
    } 
}
publicclass Test {
 publicstaticvoid main(String[] args) {
 final NameList nl =new NameList(); 
        nl.add("aaa");
 class NameDropperextends Thread{ 
 publicvoid run(){
                String name = nl.removeFirst(); 
                System.out.println(name); 
            } 
        } 

        Thread t1 = new NameDropper();
        Thread t2 = new NameDropper();
        t1.start(); 
        t2.start(); 
    } 
}

虽然集合对象

private List nameList = Collections.synchronizedList(new LinkedList()); 是同步的,但是程序还不是线程安全的。

出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。

解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:

publicclass NameList {
 private List nameList = Collections.synchronizedList(new LinkedList());

 publicsynchronizedvoid add(String name) {
        nameList.add(name); 
    } 

 publicsynchronized String removeFirst() {
 if (nameList.size() > 0) {
 return (String) nameList.remove(0);
        } else {
 returnnull;
        } 
    } 
}

这样,当一个线程访问其中一个同步方法时,其他线程只有等待。

七、线程死锁

死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。

还是看一个比较直观的死锁例子:

publicclass DeadlockRisk {
 privatestaticclass Resource {
 publicint value;
    } 

 private Resource resourceA =new Resource(); 
 private Resource resourceB =new Resource(); 

 publicint read() {
 synchronized (resourceA) {
 synchronized (resourceB) {
 return resourceB.value + resourceA.value;
            } 
        } 
    } 

 publicvoid write(int a,int b) { 
 synchronized (resourceB) {
 synchronized (resourceA) {
                resourceA.value = a; 
                resourceB.value = b; 
            } 
        } 
    } 
}

假设read()方法由一个线程启动,write()方法由另外一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,两者都坚持等待的话就出现死锁。

实际上,上面这个例子发生死锁的概率很小。因为在代码内的某个点,CPU必须从读线程切换到写线程,所以,死锁基本上不能发生。

但是,无论代码中发生死锁的概率有多小,一旦发生死锁,程序就死掉。有一些设计方法能帮助避免死锁,包括始终按照预定义的顺序获取锁这一策略。已经超出SCJP的考试范围。

八、线程同步小结

1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。

2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。

3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

4、对于同步,要时刻清醒在哪个对象上同步,这是关键。

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。

6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

Java线程:线程的交互

线程交互是比较复杂的问题,SCJP要求不很基础:给定一个场景,编写代码来恰当使用等待、通知和通知所有线程。

一、线程交互的基础知识

SCJP所要求的线程交互知识点需要从java.lang.Object的类的三个方法来学习:

void notify() 唤醒在此对象监视器上等待的单个线程。 void notifyAll() 唤醒在此对象监视器上等待的所有线程。 void wait() 导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。

当然,wait()还有另外两个重载方法:

void wait(long timeout) 导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者超过指定的时间量。 void wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

以上这些方法是帮助线程传递线程关心的时间状态。

关于等待/通知,要记住的关键点是:

必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。

wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知)。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的notify()方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。

下面看个例子就明白了:

/** 
* 计算输出其他线程锁计算的数据
* 
* @author leizhimin 2008-9-15 13:20:38 
*/ 
publicclass ThreadA {
 publicstaticvoid main(String[] args) {
        ThreadB b = new ThreadB();
 //启动计算线程
        b.start(); 
 //线程A拥有b对象上的锁。线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者
 synchronized (b) {
 try {
                System.out.println("等待对象b完成计算。。。");
 //当前线程A等待
                b.wait(); 
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            } 
            System.out.println("b对象计算的总和是:" + b.total);
        } 
    } 
}
/** 
* 计算1+2+3 ... +100的和
* 
* @author leizhimin 2008-9-15 13:20:49 
*/ 
publicclass ThreadBextends Thread {
 int total; 

 publicvoid run() {
 synchronized (this) {
 for (int i = 0; i < 101; i++) {
                total += i; 
            } 
 //(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程A被唤醒
            notify(); 
        } 
    } 
}

等待对象b完成计算。。。 b对象计算的总和是:5050 Process finished with exit code 0

千万注意:

当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。如果线程荣然在完成同步代码,则线程在移出之前不会放弃锁。因此,只要调用notify()并不意味着这时该锁变得可用。

二、多个线程在等待一个对象锁时候使用notifyAll()

在多数情况下,最好通知等待某个对象的所有线程。如果这样做,可以在对象上使用notifyAll()让所有在此对象上等待的线程冲出等待区,返回到可运行状态。

下面给个例子:

/** 
* 计算线程 
* 
* @author leizhimin 2008-9-20 11:15:46 
*/ 
publicclass Calculatorextends Thread {
 int total;

 publicvoid run() {
 synchronized (this) {
 for (int i = 0; i < 101; i++) {
                                total += i; 
                        } 
                } 
 //通知所有在此对象上等待的线程
                notifyAll(); 
        } 
}
/** 
* 获取计算结果并输出 
* 
* @author leizhimin 2008-9-20 11:15:22 
*/ 
publicclass ReaderResultextends Thread {
        Calculator c; 

 public ReaderResult(Calculator c) {
 this.c = c;
        } 

 publicvoid run() {
 synchronized (c) {
 try {
  System.out.println(Thread.currentThread() + "等待计算结果。。。");
                                c.wait(); 
                        } catch (InterruptedException e) {
                                e.printStackTrace(); 
                        } 
  System.out.println(Thread.currentThread() + "计算结果为:" + c.total);
                } 
        } 

 publicstaticvoid main(String[] args) {
                Calculator calculator = new Calculator();

 //启动三个线程,分别获取计算结果
 new ReaderResult(calculator).start();
 new ReaderResult(calculator).start();
 new ReaderResult(calculator).start();
 //启动计算线程
                calculator.start(); 
        } 
}

运行结果:

Thread[Thread-1,5,main]等待计算结果。。。 Thread[Thread-2,5,main]等待计算结果。。。 Thread[Thread-3,5,main]等待计算结果。。。 Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner at java.lang.Object.notifyAll(Native Method) at threadtest.Calculator.run(Calculator.java:18) Thread[Thread-1,5,main]计算结果为:5050 Thread[Thread-2,5,main]计算结果为:5050 Thread[Thread-3,5,main]计算结果为:5050 Process finished with exit code 0

运行结果表明,程序中有异常,并且多次运行结果可能有多种输出结果。这就是说明,这个多线程的交互程序还存在问题。究竟是出了什么问题,需要深入的分析和思考,下面将做具体分析。

实际上,上面这个代码中,我们期望的是读取结果的线程在计算线程调用notifyAll()之前等待即可。但是,如果计算线程先执行,并在读取结果线程等待之前调用了notify()方法,那么又会发生什么呢?这种情况是可能发生的。因为无法保证线程的不同部分将按照什么顺序来执行。幸运的是当读取线程运行时,它只能马上进入等待状态----它没有做任何事情来检查等待的事件是否已经发生。 ----因此,如果计算线程已经调用了notifyAll()方法,那么它就不会再次调用notifyAll(),----并且等待的读取线程将永远保持等待。这当然是开发者所不愿意看到的问题。

因此,当等待的事件发生时,需要能够检查notifyAll()通知事件是否已经发生。

原文发布于微信公众号 - Java帮帮(javahelp)

原文发表时间:2017-12-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏从零开始学自动化测试

python笔记20-获取当前运行函数名

写完代码之后,一般为了方便查看日志,可以在日志输出中加入当前运行的函数名称或类和方法名称,以便于代码报错的时候能快速找到报错的是哪个函数或方法。 那么如何获取当...

15010
来自专栏青玉伏案

ReactiveSwift源码解析(十二) MutableProperty基本代码实现

前两篇博客我们分别聊了ReactiveSwift框架中的负责标记对象的生命周期的类Lifetime以及负责原子性操作的Atomic类的具体代码实现。前两篇博客之...

19750
来自专栏java一日一条

Java代码编译和执行的整个过程

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

12920
来自专栏angularejs学习篇

页面上通过地址栏传值时出现乱码的两种解决方法

21620
来自专栏Felix的技术分享

理解对C++裸指针释放后重用的问题

24490
来自专栏大闲人柴毛毛

Java并发编程的艺术(三)——volatile

1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行;但往往多条线程之间需要共享数据,此时在并...

44270
来自专栏java一日一条

JAVA 动态代理

为了使代理类和被代理类对第三方有相同的函数,代理类和被代理类一般实现一个公共的interface,该interface定义如下

10130
来自专栏marsggbo

python编码问题

python编码问题 解决方法 python 编码 sys 在用python的时候经常会遇到编码乱码的问题,这时就需要用到sys模块。具体代码如下: impor...

23650
来自专栏黑泽君的专栏

java基础学习_多线程01_多线程_day23总结

7920
来自专栏程序员互动联盟

【编程基础】你是否真的了解main()函数?

最近看到很多人、甚至市面上的一些书籍,都使用了void main() ,其实这是错误的。C/C++中从来没有定义过void main() 。C++之父 Bjar...

33460

扫码关注云+社区

领取腾讯云代金券