一. 线程的同步与锁
1. 同步问题提出
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
例如:两个线程都操作同一个对象Foo对象,并修改Foo对象上的数据。
package com.zth;
class Foo{
private int x = 100;
public int getX(){
return x;
}
public int fix(int y){
x = x-y;
return x;
}
}
public class Demo0 implements Runnable{
private Foo foo = new Foo();
public static void main(String[] args){
Demo0 r = new Demo0();
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
@Override
public void 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());
}
}
public int fix(int y){
return foo.fix(y);
}
}
执行结果:
线程2:当前 foo 对象的 x 值= 40
线程1:当前 foo 对象的 x 值= 40
线程2:当前 foo 对象的 x 值= -20
线程1:当前 foo 对象的 x 值= -20
线程2:当前 foo 对象的 x 值= -80
线程1:当前 foo 对象的 x 值= -80
从结果发现,这样的输出值明显是不合理的,原因是两个线程不加控制的访问Foo对象并修改其数据所致。
如果要保持结果的合理性,只需要达到一个目的,就是将对Foo的访问加以限制,每次只能有一个线程在访问。这样就能保证Foo对象中数据的合理性了。
在具体的Java代码中需要完成以下两个操作:
把竞争访问的资源类Foo变量x标识为private;
同步修改变量的代码,使用synchronized关键字同步方法或代码。
2. 同步和锁定
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;
}
}
3. 静态方法同步
要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。
public static synchronized int setName(String name){
Xxx.name = name;
}
等价于:
public static intsetName(String name){
synchronized(Xxx.class){
Xxx.name = name;
}
}
4. 如果线程不能获得锁会怎么样
如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的一种池中,必须在那里等待,直到其锁被释放,该线程再次变为可运行或运行为止。
当考虑阻塞时,一定要注意哪个对象正被用于锁定:
1、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
2、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。
3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。
4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。
5. 线程安全类
package com.zth;
import javax.xml.soap.Name;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
class NameList{
private List nameList = Collections.synchronizedList(new LinkedList());
public void add(String name){
nameList.add(name);
}
public String removeFirst(){
if(nameList.size()>0){
return (String) nameList.remove(0);
}else {
return null;
}
}
}
public class Demo0{
public static void main(String[] args){
final NameList n1 = new NameList();
n1.add("zth");
class NameDropper extends Thread{
@Override
public void run() {
String name = n1.removeFirst();
System.out.println(name);
}
}
Thread t1 = new NameDropper();
Thread t2 = new NameDropper();
t1.start();
t2.start();
}
}
执行结果:
zth
Exception in thread "Thread-1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
虽然集合对象
private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,但是程序还不是线程安全的。
出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。
解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:
class NameList{
private List nameList = Collections.synchronizedList(new LinkedList());
public synchronized void add(String name){
nameList.add(name);
}
public synchronized String removeFirst(){
if(nameList.size()>0){
return (String) nameList.remove(0);
}else {
return null;
}
}
}
这样,当一个线程访问其中一个同步方法时,其他线程只有等待。
6. 线程死锁
public class Deadlock {
private static class Resource{
public int value;
}
private Resource resourceA=new Resource();
private Resource resourceB=new Resource();
public int read(){
synchronized (resourceA) {
synchronized (resourceB) {
return resourceB.value+resourceA.value;
}
}
}
public void write(int a,int b){
synchronized(resourceB){
synchronized (resourceA) {
resourceA.value=a;
resourceB.value=b;
}
}
}
}
假设read()方法由一个线程启动,write()方法由另外一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,两者都坚持等待的话就出现死锁。
7. 线程同步小结
1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的。
二. 线程的交互
1. 线程交互的基础知识
线程交互知识点需要从 java.lang.Object 的类的三个方法来学习:
void notify()——唤醒在此对象监视器上等待的单个线程。
void notifyAll()——唤醒在此对象监视器上等待的所有线程。
void wait()——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。
wait()还有另外两个重载方法:
void wait(longtimeout)——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者超过指定的时间量。
void wait(longtimeout, int nanos)——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知)。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的notify()方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。
package com.zth;
class ThreadA extends Thread{
int total = 0;
@Override
public void run() {
synchronized (this){
for(int i= 0;i<101;i++){
total += i;
}
notify();
}
}
}
public class Demo0{
public static void main(String[] args) throws Exception {
ThreadA t = new ThreadA();
t.start();
synchronized (t){
System.out.println("等待对象t完成计算......");
t.wait();
System.out.println("b对象计算的总和是:" + t.total);
}
}
}
执行结果:
等待对象t完成计算......
b对象计算的总和是:5050
当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。
多个线程在等待一个对象锁时候使用notifyAll()
在多数情况下,最好通知等待某个对象的所有线程。如果这样做,可以在对象上使用notifyAll()让所有在此对象上等待的线程冲出等待区,返回到可运行状态。
三. 守护线程
守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
setDaemon方法的详细说明:
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。
该方法必须在启动线程前调用。
该方法首先调用该线程的 checkAccess方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
参数:on - 如果为true,则将该线程标记为守护线程。
package com.zth;
class MyCommon extends Thread{
@Override
public void run() {
for(int i = 1;i<=3;i++){
System.out.println("线程1第"+i+"次执行!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyDaemon implements Runnable{
@Override
public void run() {
for(int i = 1;i<1000;i++){
System.out.println("后台线程第"+i+"次执行!");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo0{
public static void main(String[] args){
Thread t1 = new MyCommon();
Thread t2 = new Thread(new MyDaemon());
t2.setDaemon(true);
t1.start();
t2.start();
}
}
执行结果:
线程1第1次执行!
后台线程第1次执行!
后台线程第2次执行!
线程1第2次执行!
后台线程第3次执行!
后台线程第4次执行!
线程1第3次执行!
后台线程第5次执行!
后台线程第6次执行!