java多线程之线程同步

2017-01-13 15:06:36来源:csdn作者:ksj_j人点击


线程不同步问题的出现


当处理共享资源的时候,修改数据和读取数据的同时,多线程不同步会出现脏数据,注意脏数据只是数据错处,并不是说代码逻辑有问题,代码本身是没有什么问题的。举个栗子,共享数据为i,i初始值为1,假设线程分为a,b并且线程不同步。


首先假设我们需要做的操作为:

int a = i;
a++;
i = a;上面代码为两个线程需要执行的代码,当a线程执行代码的时候,局部变量a = i的操作的时候a为1,此时假设还没有执行a++操作,线程b获取资源执行代码,此时b线程对应的局部变量a也为1,然后分别执行a++,然后在赋值,两个线程中的i结果都等于2,我们需要的结果应该是3(至少要一个为3),因为两个线程分别执行这个方法,又因为i是共享资源,所以i应该变为3,由于线程不同步出现了脏数据。有人又会提出质疑,直接i++不久结束了吗?但是java代码执行i++,并不是单纯的执行a++ 一步操作即可,底层是分为多步执行的,既然分步就会有先后也会造成刚刚出现的问题。这个就是非原子性造成的结,这里原子性就是最小的操作·,不能分割了的操作。

接下来我使用代码作为实例演示一下问题具体所在:

public class TestThread {public static void main(String[] args) {
Ticket ticket = new Ticket();
//创建两个线程子类
Customer s1 = new Customer(ticket);
Customer s2 = new Customer(ticket);
s1.start();
s2.start();
}
}class Ticket{
//车票数量
private int number = 100000;
final Object object = new Object();
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
//买票
public void buyTicket() {
if(number > 0){
number--;
}
}
}class Customer extends Thread{
private Ticket ticket;
@Override
public void run() {
for(int i = 0; i < 10000; i++){
ticket.buyTicket();
System.out.println("线程名"+getName()+" : 剩余火车票"+ticket.getNumber());
if(ticket.getNumber() == 0) break;
}
}
public Customer(Ticket ticket) {
this.ticket = ticket;
}
}结果:线程名Thread-0 : 剩余火车票80064
线程名Thread-0 : 剩余火车票80063
线程名Thread-0 : 剩余火车票80062
线程名Thread-0 : 剩余火车票80061
线程名Thread-0 : 剩余火车票80060
线程名Thread-0 : 剩余火车票80059
线程名Thread-0 : 剩余火车票80058
线程名Thread-0 : 剩余火车票80057

这里最终结果为80057,也就是车票还剩这么多,但是我设置的每个线程都会自动买10000张车票,这里开了两个线程,结果应该为80000才对,原因在于线程不同步。



解决线程同步问题简单的分成5种


使用synchronized修饰符

public synchronized void buyTicket() {
if(number > 0){
number--;
}
}

其他代码不变,在这个方法上加上一个synchronized,说一下原理,首先java每个对象都有一个内置锁,这里的内置锁是这个类(Ticket),有了这个修饰符就代表,只有这个方法不能同时被多个线程调用,只有其中一个调用完之后才可以让其他线程(包括自己)争夺线程。这时候就可以把整个方法看作一个原子,也就具有了原子性。



使用同步代码块,这里还是使用synchronized

public void buyTicket() {
synchronized (object) {
if(number > 0){
number--;
}
}
}

还是将那块代码改变成这样,这个和上面差不多,但是值得一说的线程同步是非常消耗资源的,如果要在这两种选择的话尽量使用这种,不要使用上面那种。



使用volatile特殊变量

private volatile int number = 100000;//volatile修饰变量

volatile在上述场是没有用的。因为volatile只能保证可见性,可见性就是一个线程修改了一个数据,另一个线程是可见的,但是他不具有原子性,所以在遇到非原子性操作的时候是没有用的。



使用ReentrantLock类

Lock lock = new ReentrantLock();
publicvoid buyTicket() {
lock.lock();
try {
if(number > 0){
number--;
}
}finally{
lock.unlock();
}
}这种方法很简单理解,将需要同步的代码上锁就好了。这个和synchronized差不多都堵塞线程,所以其实都是好消耗资源的操作。需要注意的是使用了锁,需要解锁。解锁最好在finally种执行,以免线程导致死锁。使用Threadlocal类
private ThreadLocal<Integer> number = new ThreadLocal<Integer>() {
//初始值设置
@Override
protected Integer initialValue() {
return 100000;
}
};
publicvoid buyTicket() {
if(number.get() > 0){
number.set(number.get()-1);
}
}注意这个已经不是数据共享的问题了。结果太长不好贴出,描述一下最后结果:线程名Thread-0 : 剩余火车票90000最后结果:线程名Thread-1 : 剩余火车票90000结果就是每个线程都执行了10000遍,但是数据不会相互交互,


从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。


上面那个栗子不能体现这种用法的好处,接下来我再写一个栗子。



ThreadLocalTest.java

public class ThreadLocalTest implements Runnable{private final static ThreadLocal<User> userTL =new ThreadLocal<>();public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
Thread t1 = new Thread(test,"线程a");
Thread t2 = new Thread(test,"线程b");
t1.start();
t2.start();
}
@Override
public void run() {
User user = userTL.get();
if(user == null){
userTL.set(new User());
}System.out.println(userTL.get());
user = userTL.get();
user.setMoney((int)(Math.random()*1000));
for(int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName()+":用户拥有金额"+user.getMoney());
}
}}

User.java

public class User {private int money;public int getMoney() {
return money;
}public void setMoney(int money) {
this.money = money;
}}结果:线程a:用户拥有金额156
线程b:用户拥有金额710
线程a:用户拥有金额156
线程b:用户拥有金额710
线程a:用户拥有金额156
线程b:用户拥有金额710
线程a:用户拥有金额156
线程b:用户拥有金额710
线程a:用户拥有金额156
线程b:用户拥有金额710

两个用户数据不会因为线程而发生任何交互,这样就达到了线程安全。











最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台