1. 信息共享
多线程中最重要的一点就是并发问题,但是对于刚开始学的时候并不是很清楚并发到底是用来解决什么问题的。很难去想到一个合理的例子表达出并发的想要解决的问题。
- 粗粒度:子线程和子线程之间以及main线程之间缺乏信息交流
- 细粒度:线程之间有信息交流通讯,线程之间有数据的共享交流。
假设现在一共有100张票要售出,现在有四个售票点一起卖这100张票,那么首先要明确这四个售票点一共只能卖100张,不能多卖但是可以少卖。
- 关于继承Thread类的信息共享问题,继承Thread类的子类在实现多线程信息共享中需要借助静态变量来完成,否则每个线程都单独执行自己的数据不会进行数据交流。这种情况下每个线程都是一个单独的个体,类似于每个售票点都有100张可以卖,那么一共卖了400张并没有实现四个售票点之间的数据共享。
public class Test extends Thread{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+" --> 卖出的票号: "+ticket);
ticket = ticket - 1;
}
}
public static void main(String[] args) {
Test t1 = new Test(); //售票点1有100张票
Test t2 = new Test(); //售票点2有100张票
Test t3 = new Test(); //售票点3有100张票
Test t4 = new Test(); //售票点4有100张票
t1.start();t2.start();
t3.start();t4.start();
}
}
- 如果将票数改为static的变量,那么四个售票点就实现了数据的共享,虽然卖出的票数可能会超过100张而且卖出的票号顺序也不对,但是卖出的数量也肯定远远小于400的,这样就是一个小并发。至于卖出的票数为什么会超过100张这是并发加锁问题待会再讨论。
public class Test extends Thread{
private static int ticket = 100; //静态变量实现数据共享
@Override
public void run() {
while(true){
if(ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+" --> 卖出的票号: "+ticket);
ticket = ticket - 1;
}
}
public static void main(String[] args) {
Test t1 = new Test();
Test t2 = new Test();
Test t3 = new Test();
Test t4 = new Test();
t1.start();t2.start();
t3.start();t4.start();
}
}
- Runnable接口是采用静态代理的方式,那么实现了这个接口的类直接通过普通的变量就能实现数据的共享。四个代理点共同卖100张票
public class TestRunnable implements Runnable{
private int ticket = 100; //普通变量实现数据共享
@Override
public void run() {
while(true){
if(ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+" --> 卖出的票号: "+ticket);
ticket = ticket - 1;
}
}
public static void main(String[] args) {
TestRunnable t = new TestRunnable(); //总票数100
new Thread(t).start(); //静态代理1
new Thread(t).start(); //静态代理2
new Thread(t).start(); //静态代理3
new Thread(t).start(); //静态代理4
}
}
总结:继承Thread类的子类想要实现数据共享必须采用static静态变量,这样每一个派生出来的子类就可以共享这一份static变量。而实现Runnable接口的类直接就可以通过普通变量来实现数据的共享,采用静态代理的设计模式来实现的。
2. 数据不一致问题
上面出现的数据不一致问题是因为线程之间的数据不共享存在工作缓存副本,关键步骤没有上锁。由于每个线程都有一个工作缓存区,从主存中取数据存放在缓存工作区,然后对当前线程如果修改了数据还是针对缓存中的数据并没有会写到主内存RAM中,更没有通知其他线程。因此每个缓存中的数据可能都是修改之前的数据,这样才导致了数据不一致问题,为了解决这一类问题需要去了解 可见性 和 原子性 以及 有序性。
- 可见性:是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量直接跳过线程内部缓存工作区即直接修改内存,所以对其他线程是可见的。但是这里需要注意只有volatile修饰的内容才具有可见性,但不能保证它具有原子性。例如 volatile int i = 0;之后有一个操作 i++;这个变量i具有可见性,但是i++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
public class TestVolatile implements Runnable{
volatile private int tikcet = 100;
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName()+"卖出的票号: "+tikcet);
tikcet--;
if(tikcet <= 0){
break;
}
}
}
public static void main(String[] args) {
TestVolatile t = new TestVolatile();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
- 原子性:原子是构成物质的最小单位,因此具有不可以再分割性,例如 i = 0这个操作是一个原子操作,但是 i = i + 1这是一个非原子操作。所有的非原子操作线程都不安全,对于解决这种线程不安全问题需要使用线程同步技术(sychronized),将这些关键步骤设立为临界区,所谓临界区就是依次只能有一个线程区访问,多线程访问时必须按顺序依次访问(串行)。
//正确姿势
public class Test implements Runnable{
volatile private int tikcet = 100;
@Override
public void run() {
while(true){
sale();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tikcet <= 0){
break;
}
}
}
public synchronized void sale(){ //同步函数,临界区
System.out.println(Thread.currentThread().getName()+"卖出的票号: "+tikcet);
tikcet--;
}
public static void main(String[] args) {
Test t = new Test();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
- 有序性:Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 其本身包含 禁止指令重排序 的意思,synchronized 是由 一个变量在同一个时刻只允许一条线程对其进行加锁操作设立为临界区,因此多线程访问时只能串行。
总结:Java中使用volatile 和 synchronized解决线程之间数据不同步的问题,volatile只解决了可见性和有序性,synchronized解决了可见性,有序性,以及原子性。但是在单例设计模式中只使用synchronized还是会出现有序性的问题,因此在单例的设计模式中会使用双锁,即两个关键词一起用。