线程同步和安全问题
要使多道程序能够并发执行,不同的线程间就不可避免地需要对一些共享资源进行操作。而在操作的过程中,可能发生一些隐患。如果只看一个线程执行的代码,它是被顺序执行的。但是如果观测所有线程的代码,会发现所有线程的代码并不是按照一个固定的顺序去执行的。
public class example {
public static void main(String[] args) {
examplethread test1 = new examplethread();
examplethread test2 = new examplethread();
test1.start();
test2.start();
}
}
public class examplethread extends Thread{
public void run() {
int i = 1000;
while(i>0) {
System.out.println(this.getId()+" "+i);
i--;
}
}
}
上面这段代码并不能做到让按顺序交替输出,其运行结果片段如下:
当使用多个线程访问共享资源时,由于这种现象,可能导致我们访问到的共享资源并不是我们希望的那个结果,这就是出现了线程安全问题。
为了解决线程的安全问题,就需要引入一个同步机制,来规范不同线程对共享资源的访问,使得访问到的就是我们所希望的结果。
java中提供了synchronized和lock两种实现同步的方法。在Java中每个对象都有一个锁,多个线程要想访问该对象,需要持有该对象的锁才能访问。
Synchronized
Synchronized关键字是java的内置关键字,可以通过Synchronized来定义同步块或者同步方法来实现同步。
同步块:
synchronized(Object){
}
当某个线程执行到这样的synchronized代码块时,会得到对象Object的锁,从而使得这段Object对象相关的代码只能由持有锁的这个线程执行,以此来实现线程同步。
同步方法:
在方法前加synchronized关键字,该方法就被定义为同步方法。当有一个线程调用这个synchronized方法时,这个线程就获得了这个synchronized方法的对象的锁,其他线程也就不能再访问这个对象的synchronized方法。
必须将每个访问共享资源的方法都定义为synchronized方法。
Lock
Lock不是Java内置的关键字,而是一个接口,在java.util.concurrent.locks包下。
lock接口的定义如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock、trylock、方法都可以用来获取锁,不过trylock方法不会等待,不管是否获得锁都立即返回。如果获得锁,返回true,没获得返回false。
lock方法如果没有获得锁,则等待。因此通常lock方法要和异常处理一起使用。
Lock接口有唯一的一个实现类ReentrantLock,实例化一个ReentrantLock对象,该对象就代表该类的锁,调用该对象的Lock接口方法,就可以来获得或者释放这个类(的对象)的锁。
哲学家进餐
哲学家进餐是个经典的操作系统进程同步问题,问题描述如下:
五个哲学家围着一张桌子坐下,每两人之间有一支筷子,要想吃饭的话需要同时拿两只筷子,他们的生活方式是交替进行吃饭。
这个问题中,筷子就是临界资源,哲学家之间如果不沟通,同时拿起同侧的一支筷子就会发生死锁。
这个问题目前有三种主流解法:
服务生解法:引入一个服务生,当哲学家想要拿起筷子时,服务生要先判断是不是两支筷子都空闲,拿起来能就餐。当可以时才让哲学家拿筷子。
资源分级解法:为资源分配一个分级关系,按该关系分配资源,按该关系相反的关系释放资源。为每个筷子设置递增(递减)的等级,哲学家先拿左右两边筷子中级低的那支,再拿级高的那支。
Chandy/Misra解法。
用synchronized实现
下面这段代码分别用synchronized关键字和lock接口模仿服务生解法来解决哲学家进餐问题。
import java.util.*;
public class SynchronizedMethod {
public volatile List<Integer> chopsticks = new ArrayList<Integer>();
//创建哲学家列表
List<LazyDogs> dogs = new ArrayList<LazyDogs>();
public SynchronizedMethod() {
int a = 6;
//初始化筷子列表
for(int i=0;i<5;i++) {
chopsticks.add(a);
}
// for(int i=0;i<chopsticks.size();i++) {
// System.out.println("编号"+i+"的筷子为"+chopsticks.get(i));
// }
}
}
import java.util.*;
public class LazyDogs extends Thread{
private List<Integer> chopsticks;
//哲学家的位置
public int location;
public LazyDogs(List<Integer> chopsticks) {
//将筷子列表传递多来
this.chopsticks = chopsticks;
}
public void run() {
// System.out.println(location+"已启动");
while(true) {
Integer one = chopsticks.get(location);
//获得一边的筷子的锁
synchronized(one) {
Integer two = chopsticks.get((location+1)%5);
//获得另一边的筷子的锁
synchronized(two) {
//模仿服务生 检查如果拿起筷子是否能够吃饭
if(chopsticks.get(location)==6&&chopsticks.get((location+1)%5)==6) {
chopsticks.set(location, location);
System.out.println(location+"拿起了筷子"+location);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
chopsticks.set((location+1)%5, location);
System.out.println(location+"拿起了筷子"+(location+1)%5);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(location+"正在吃饭");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//吃完饭后放下筷子
chopsticks.set(location, 6);
System.out.println(location+"放下了筷子"+location);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
chopsticks.set((location+1)%5, 6);
System.out.println(location+"放下了筷子"+(location+1)%5);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
break;
}
}
}
}
}
}
public class test {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建筷子对象
SynchronizedMethod sm = new SynchronizedMethod();
//创建哲学家对象
for(int i = 0;i<5;i++) {
LazyDogs dog = new LazyDogs(sm.chopsticks);
dog.location = i;
sm.dogs.add(dog);
}
//启动线程
for(int i=0;i<sm.dogs.size();i++) {
sm.dogs.get(i).start();
}
}
}
结果如下:
可以看到,虽然没有发生死锁,但是每次只有一个哲学家进餐,并发程度比较低。
用synchronized关键字来获得锁,获得锁的范围只持续到那个代码块结束,结束后锁会被自动释放,因此使用时不够灵活。
用Lock实现
import java.util.concurrent.locks.*;
public class chopstick {
//创建一个可重入锁对象
public Lock lock = new ReentrantLock();
//表明筷子是否被拿起 此时没有被拿起
int flag = 1;
}
import java.util.ArrayList;
import java.util.List;
public class chopsticks {
//创建筷子列表
public volatile List<chopstick> cs = new ArrayList<chopstick>();
//将筷子加入筷子列表
public chopsticks() {
for(int i =0;i<5;i++) {
chopstick temp = new chopstick();
cs.add(temp);
}
}
}
import java.util.ArrayList;
import java.util.List;
public class LockDogs extends Thread{
private List<chopstick> chopsticks = new ArrayList<chopstick>();
public int location;
//将筷子列表传给哲学家
public LockDogs(List<chopstick> chopsticks) {
this.chopsticks = chopsticks;
}
public void run() {
while(true) {
//获得左右两边筷子的锁
chopsticks.get(location).lock.lock();
chopsticks.get((location+1)%5).lock.lock();
try {
//模仿服务生 判断取筷子后是否能吃饭
if(chopsticks.get(location).flag==1&&chopsticks.get((location+1)%5).flag==1) {
//拿筷子 吃饭 放筷子
chopsticks.get(location).flag = 0;
System.out.println(location+"拿起了"+location);
Thread.sleep(1000);
chopsticks.get((location+1)%5).flag = 0;
System.out.println(location+"拿起了"+(location+1)%5);
Thread.sleep(1000);
System.out.println(location+"正在吃饭");
Thread.sleep(1000);
chopsticks.get(location).flag = 1;
System.out.println(location+"放下了"+location);
Thread.sleep(1000);
chopsticks.get((location+1)%5).flag = 1;
System.out.println(location+"放下了"+(location+1)%5);
Thread.sleep(1000);
break;
}
}catch(Exception e) {
}finally {
//若此时两边筷子不全可用则释放锁 继续检查条件
chopsticks.get(location).lock.unlock();
chopsticks.get((location+1)%5).lock.unlock();
}
}
}
}
结果如下:
可以看到,用Lock实现满足条件的哲学家可以同时吃饭,由于锁的释放是手动的,在使用时更加灵活,并发程度更好。
synochronized和Lock的内容还有很多,这只是简单的应用,背后的深入还有待了解。