线程的同步问题
线程的同步是为了防止多个线程同时访问一个数据对象时,对数据造成的破坏。
例如:
Thread-a和Thread-b同时操作一个数据Foo对象
/**
* @author FengTianHao
* @version 1.0
* @since 2018/11/18 16:52
*/
public class Fool {
private int x=100;
public int getX()
{
return x;
}
public void fix(int y)
{
x=x-y;
}
}
/**
@author FengTianHao
@version 1.0
@since 2018/11/15 17:04
*/
public class TestThread implements Runnable {
private Fool foo=new Fool();
@Override
public void run()
{
for(int i=0;i<3;i++)
{
foo.fix(30);
System.out.println(Thread.currentThread().getName()+foo.getX());
try {
Thread.sleep(30);//每到线程执行到这时会休眠30毫秒,给其他线程提供执行的机会
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args)
{
TestThread thread=new TestThread();
Thread a=new Thread(thread,"Thread-a");
Thread b=new Thread(thread,"Thread-b");
a.start();
b.start();
}
}
运行结果:
这样的结果显然是不合理的,同时访问一个数据,结果应该是相同的,但上面运行结果却有不相同的时候,这就是对两个线程不加以控制访问Fool对象造成的。
要想结果具有合理性,我们应该对访问数据的线程做一些规定,例如每次只有一个线程能访问,这样就能保证数据的合理性。
在具体的java代码中通常采取以下两个操作:
1.将多个线程要访问的数据对象设为private型
2.同步哪些修改变量的代码时,用关键字synchronized同步方法或者代码块
同步和锁定
锁的原理
- java中每个对象都有一个内置锁
- 当程序运行到非静态的synchronized同步方法时,自动获得与正在中代码类的当前实例有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上同步、在对象上锁定
- 当程序运行到synchronized同步的方法或者代码块时,对象锁才起作用
- 一个对象只有一个锁,如果一个线程获得该锁,则没有其他线程可以获得该锁,除非获得该锁的线程将之释放。这意味着其他线程不能进入该对象的synchronized同步方法或者代码块。
- 释放锁是指持有该锁的线程退出了synchronized同步方法或者代码块。
注意
- 只能同步方法不能同步类和变量
- 每个对象只有一个锁,提起同步的时候要清楚在哪个对象上同步
- 不必同步所有的方法,类可以同时拥有同步方法和非同步方法
- 如果两个线程同时访问一个类中的synchronized方法,则只有持锁线程才能访问该方法,其他的线程只有等到该锁被释放才能获得该锁并访问该方法。
- 如果类有同步和非同步方法,非同步方法可以被多个线程任意访问而没有限制
- 线程睡眠时,它持有的锁不会被释放
- 线程可以获得多个锁,例如在一个同步方法里面调用另一个同步方法,此时该线程获得两把锁。
静态方法同步
要同步静态方法,需要一个用于整个类对象的锁,这个对象就是这个类(xxx.class)例如:
public static synchronized int setName(String name)
{
Xxx.name=name;
}
等价于
public static int setName(String name)
{
synchronized (Xxx.class)
{
Xxx.name=name;
}
}
如果线程不能获得锁会如何
如果线程企图进入同步方法,而其锁已经别占用,则线程会在该对象上被阻塞直到该对象上的锁被释放,该线程再次进入可执行状态。
当考虑阻塞时,一定要明白哪个对象被锁定:
- 调用同一个对象中的非静态方法的线程彼此阻塞,如果是不同对象则线程间彼此不干扰。
- 调用同一个类中的静态方法的线程彼此阻塞,它们锁定在相同的class对象。
- 静态同步方法和非静态同步方法永远不会阻塞,因为静态同步方法锁定在class对象上,非静态同步方法锁定在该类的对象上。
- 对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)在同一个对象上同步的线程彼此会阻塞,在不同对象上的同步的线程不会阻塞。
何时需要同步
在多个线程同时访问互斥数据时,应该同步来保护数据,确定两个线程不会同时对该数据进行修改。
线程安全类
- 当一个类很好的用同步来保护它的数据时就称之为线程安全类
- 即是是线程安全类也可能会出现问题,因为操作它的线程可能是不安全的
- 举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。
代码如下:
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* @author FengTianHao
* @version 1.0
* @since 2018/11/19 18:26
*/
public class Namelist {
private List namelist= Collections.synchronizedList(new LinkedList());
public void addName(String name)
{
namelist.add(name);
}
public String RemoveFirst()
{
if(namelist.size()>0)
{
return (String)namelist.remove(0);
}
else {
return null;
}
}
}
/**
* @author FengTianHao
* @version 1.0
* @since 2018/11/15 17:04
*/
public class TestThread {
public static void main(String[] args)
{
final Namelist namelist=new Namelist();
namelist.addName("aaa");
class ThreadTest extends Thread
{
@Override
public void run()
{
String name=namelist.RemoveFirst();
System.out.println(name);
}
}
Thread t1=new ThreadTest();
Thread t2=new ThreadTest();
t1.start();
t2.start();
}
}
虽然集合对象是线程安全的,但是操作的程序确实不安全的,解决的方法是对操作的集合对象加个同步代码如下:
public synchronized String RemoveFirst()
{
if(namelist.size()>0)
{
return (String)namelist.remove(0);
}
else {
return null;
}
}
这样的话当一个线程对这个对象的同步方法进行访问时,其他线程只能等待。
线程死锁
在java程序中,如果两个线程彼此都在等待对方因而进入阻塞状态,此时就会发生死锁。如下:
/**
* @author FengTianHao
* @version 1.0
* @since 2018/11/19 18:46
*/
public class DeadLockRisk {
private static class Resource
{
public int value;
}
private Resource ra=new Resource();
private Resource rb=new Resource();
public int read()
{
synchronized(ra)
{
synchronized (rb)
{
return ra.value+rb.value;
}
}
}
public void write(int a,int b)
{
synchronized (rb)
{
synchronized (ra)
{
ra.value=a;
rb.value=b;
}
}
}
}
假如有同时有两个线程a b,a线程调用了read()方法的同时,b线程调用了write()方法,此时a线程拥有了锁ra,b线程有了锁rb,如果两个线程一直等下去的话就会发生死锁。
小结
- 线程同步的目的是为了保护多个线程访问一个资源对资源的破坏。
- 线程同步的实现是通过锁,一个对象只有一个锁,每个锁都特定对应一个对象;当一个线程获得该对象的锁时,其他线程就不能访问该对象的同步方法。
- 对于静态同步方法,锁定的是这个类的class对象,而非静态同步方法,锁定的是该类的对象。
- 对于同步要时刻清楚在哪个对象上同步。
- 多个线程等待一个对象的锁时,没有获得锁的线程将会阻塞。
- 死锁时线程之间相互等待锁造成的。