1.线程间的相互干扰
笔者上一篇文章讲到使用Runnable来实现多线程可以共享资源,但是这也带来了问题。我们来看上一篇文章的一段代码
public class MyThread implements Runnable {
private int ticket = 5;
public void run() {
while (true) {
if (ticket > 0) {
//加入休眠使效果更明显
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//输出正在运行的线程名字以及剩余票数
System.out.println(Thread.currentThread().getName()+"正在售票" + ticket--);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt=new MyThread();
//启动线程
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
当运行这段代码时可能会出现以下情况(这种错误的出现不可预测,笔者也是运行了多次程序才出现):Thread-0正在售票0
Thread-2正在售票-1
按道理来说这两种情况是不应该出现的,那么造成这种错误的原因是什么呢?
这是因为有多个线程对同时对ticket进行了操作。我们来模拟这个过程:
整个线程运行的过程是获取当前ticket的值,进入判断语句若符合条件则休眠4秒,然后输出当前ticket的值,再将ticket减1然后返回。
假设线程1获得了当前Ticket的值1,但是还没有来的及将ticket减1返回。但是此时线程0和线程2已经先后进入判断语句,就在这时ticket减1了,然后线程0输出的就是减1后的结果0了,线程0再减1返回,线程2获得的是再次减1的结果,即-1.
2.内存一致性错误
int count=0
假设有A和B两个线程共享这个变量。A线程对变量进行count++操作,线程B对变量进行输出操作System.out.println(count);
这是线程B输出的结果可能是0。因为线程A对变量的操作对线程B不可见。
解决以上两个错误的方法之一就是使用同步。
3.同步方法
定义同步方法的格式:访问修饰符 synchronized 返回值类型 方法名(数据类型 变量名){
方法体;
}
以上文中的代码为例
public class MyThread implements Runnable {
private int ticket = 5;
public void run() {
while (true) {
this.sale();
}
}
/*
*同步方法
*/
public synchronized void sale(){
if (ticket > 0) {
//加入休眠使效果更明显
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//输出正在运行的线程名字以及剩余票数
System.out.println(Thread.currentThread().getName()+"正在售票" + ticket--);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt=new MyThread();
//启动线程
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
4.同步代码块
同步代码块定义格式:synchronized(){
需要同步的代码;
}
public class MyThread implements Runnable {
private int ticket = 5;
public void run() {
while (true) {
//同步代码块
synchronized (this) {
if (ticket > 0) {
// 加入休眠使效果更明显
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 输出正在运行的线程名字以及剩余票数
System.out.println(Thread.currentThread().getName() + "正在售票" + ticket--);
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt=new MyThread();
//启动线程
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
注意:1)使用太多的同步容易出现死锁,关于死锁本篇文章不做说明,笔者将另写一篇。
2)原子性动作是指一次性完成的动作,即要么一次执行完所有的动作要么不执行,volatile声明的所有变量都是原子性动作。使用volati可以避免线程互相干扰,减少内存一致性错误。