ThreadLocal与synchronized

ThreadLocal与synchronized

Java良好的支持多线程。使用java,我们可以很轻松的编程一个多线程程序。但是使用多线程可能会引起并发访问的问题。 synchronized和ThreadLocal都是用来解决多线程并发访问的问题。大家可能对synchronized较为熟悉,而对 ThreadLocal就要陌生得多了。

并发问题。当一个对象被两个线程同时访问时,可能有一个线程会得到不可预期的结果。

一个简单的java类Studnet

public class Student {   
private int age=0;

public int getAge() {
return this.age;

}

public void setAge(int age) {
this.age = age;
}
}
一个多线程类ThreadDemo.

这个类有一个Student的私有变量,在run方法中,它随机产生一个整数。然后设置到student变量中,从student中读取设置后的值。然后睡眠5秒钟,最后再次读student的age值。

public class ThreadDemo implements Runnable{   
Student student = new Student();
public static void main(String[] agrs) {
ThreadDemo td = new ThreadDemo();
Thread t1 = new Thread(td,"a");
Thread t2 = new Thread(td,"b");
t1.start();
t2.start();

}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
accessStudent();
}

public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName+" is running!");
// System.out.println("first read age is:"+this.student.getAge());
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread "+currentThreadName +" set age to:"+age);

this.student.setAge(age);
System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());

}

}

运行这个程序,屏幕输出如下:

a is running!

b is running!

thread b set age to:33

thread b first read age is:33

thread a set age to:81

thread a first read age is:81

thread b second read age is:81

thread a second read age is:81

需要注意的是,线程a在同一个方法中,第一次读取student的age值与第二次读取值不一致。这就是出现了并发问题。

synchronized

上面的例子,我们模似了一个并发问题。Java提供了同步机制来解决并发问题。synchonzied关键字可以用来同步变量,方法,甚至同步一个代码块。

使用了同步后,一个线程正在访问同步对象时,另外一个线程必须等待。

Synchronized同步方法

现在我们可以对accessStudent方法实施同步。

public synchronized void accessStudent()

再次运行程序,屏幕输出如下:

a is running!

thread a set age to:49

thread a first read age is:49

thread a second read age is:49

b is running!

thread b set age to:17

thread b first read age is:17

thread b second read age is:17

加上了同步后,线程b必须等待线程a执行完毕后,线程b才开始执行。

对方法进行同步的代价是非常昂贵的。特别是当被同步的方法执行一个冗长的操作。这个方法执行会花费很长的时间,对这样的方法进行同步可能会使系统性能成数量级的下降。

Synchronized同步块

在accessStudent方法中,我们真实需要保护的是student变量,所以我们可以进行一个更细粒度的加锁。我们仅仅对student相关的代码块进行同步。

synchronized(this) {   
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread "+currentThreadName +" set age to:"+age);

this.student.setAge(age);

System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
}


运行方法后,屏幕输出:

a is running!

thread a set age to:18

thread a first read age is:18

b is running!

thread a second read age is:18

thread b set age to:62

thread b first read age is:62

thread b second read age is:62

需要特别注意这个输出结果。

这个执行过程比上面的方法同步要快得多了。

只有对student进行访问的代码是同步的,而其它与部份代码却是异步的了。而student的值并没有被错误的修改。如果是在一个真实的系统中,accessStudent方法的操作又比较耗时的情况下。使用同步的速度几乎与没有同步一样快。

使用同步锁

稍微把上面的例子改一下,在ThreadDemo中有一个私有变量count,。

private int count=0;

在accessStudent()中, 线程每访问一次,count都自加一次, 用来记数线程访问的次数。

try {   
this.count++;
Thread.sleep(5000);
}catch(InterruptedException ex) {
ex.printStackTrace();
}


为了模拟线程,所以让它每次自加后都睡眠5秒。

accessStuden()方法的完整代码如下:

代码
 String currentThreadName = Thread.currentThread().getName();   
System.out.println(currentThreadName+" is running!");
try {
this.count++;
Thread.sleep(5000);
}catch(InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread "+currentThreadName+" read count:"+this.count);


synchronized(this) {
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread "+currentThreadName +" set age to:"+age);

this.student.setAge(age);

System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
}
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());

运行程序后,屏幕输出:

a is running!

b is running!

thread a read count:2

thread a set age to:49

thread a first read age is:49

thread b read count:2

thread a second read age is:49

thread b set age to:7

thread b first read age is:7

thread b second read age is:7

我们仍然对student对象以synchronized(this)操作进行同步。

我们需要在两个线程中共享count失败。

所以仍然需要对count的访问进行同步操作。

synchronized(this) {   
try {
this.count++;
Thread.sleep(5000);
}catch(InterruptedException ex) {
ex.printStackTrace();
}
}
System.out.println("thread "+currentThreadName+" read count:"+this.count);


synchronized(this) {
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread "+currentThreadName +" set age to:"+age);

this.student.setAge(age);

System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
}
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
long endTime = System.currentTimeMillis();
long spendTime = endTime - startTime;
System.out.println("花费时间:"+spendTime +"毫秒");


程序运行后,屏幕输出

a is running!

b is running!

thread a read count:1

thread a set age to:97

thread a first read age is:97

thread a second read age is:97

花费时间:10015毫秒

thread b read count:2

thread b set age to:47

thread b first read age is:47

thread b second read age is:47

花费时间:20124毫秒

我们在同一个方法中,多次使用synchronized(this)进行加锁。有可能会导致太多额外的等待。

应该使用不同的对象锁进行同步。

设置两个锁对象,分别用于student和count的访问加锁。

private Object studentLock = new Object();   
private Object countLock = new Object();

accessStudent()方法如下:
long startTime = System.currentTimeMillis();
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName+" is running!");
// System.out.println("first read age is:"+this.student.getAge());

synchronized(countLock) {
try {
this.count++;
Thread.sleep(5000);
}catch(InterruptedException ex) {
ex.printStackTrace();
}
}
System.out.println("thread "+currentThreadName+" read count:"+this.count);


synchronized(studentLock) {
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread "+currentThreadName +" set age to:"+age);

this.student.setAge(age);

System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
}
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
long endTime = System.currentTimeMillis();
long spendTime = endTime - startTime;
System.out.println("花费时间:"+spendTime +"毫秒");


这样对count和student加上了两把不同的锁。

运行程序后,屏幕输出:

a is running!

b is running!

thread a read count:1

thread a set age to:48

thread a first read age is:48

thread a second read age is:48

花费时间:10016毫秒

thread b read count:2

thread b set age to:68

thread b first read age is:68

thread b second read age is:68

花费时间:20046毫秒

与两次使用synchronized(this)相比,使用不同的对象锁,在性能上可以得到更大的提升。

由此可见synchronized是实现java的同步机制。同步机制是为了实现同步多线程对相同资源的并发访问控制。保证多线程之间的通信。

可见,同步的主要目的是保证多线程间的数据共享。同步会带来巨大的性能开销,所以同步操作应该是细粒度的。如果同步使用得当,带来的性能开销是微不足道的。使用同步真正的风险是复杂性和可能破坏资源安全,而不是性能。

ThreadLocal

由上面可以知道,使用同步是非常复杂的。并且同步会带来性能的降低。Java提供了另外的一种方式,通过ThreadLocal可以很容易的编写多线程程序。从字面上理解,很容易会把ThreadLocal误解为一个线程的本地变量。其它ThreadLocal并不是代表当前线程,ThreadLocal 其实是采用哈希表的方式来为每个线程都提供一个变量的副本。从而保证各个线程间数据安全。每个线程的数据不会被另外线程访问和破坏。

我们把第一个例子用ThreadLocal来实现,但是我们需要些许改变。

Student并不是一个私有变量了,而是需要封装在一个ThreadLocal对象中去。调用ThreadLocal的set方法, ThreadLocal会为每一个线程都保持一份Student变量的副本。所以对student的读取操作都是通过ThreadLocal来进行的。

protected Student getStudent() {   
Student student = (Student)studentLocal.get();
if(student == null) {
student = new Student();
studentLocal.set(student);
}
return student;
}

protected void setStudent(Student student) {
studentLocal.set(student);
}

accessStudent()方法需要做一些改变。通过调用getStudent()方法来获得当前线程的Student变量,如果当前线程不存在一个Student变量,getStudent方法会创建一个新的Student变量,并设置在当前线程中。

Student student = getStudent();

student.setAge(age);

accessStudent()方法中无需要任何同步代码。

完整的代码清单如下:

TreadLocalDemo.java

public class TreadLocalDemo implements Runnable {   
private final static ThreadLocal studentLocal = new ThreadLocal();

public static void main(String[] agrs) {
TreadLocalDemo td = new TreadLocalDemo();
Thread t1 = new Thread(td,"a");
Thread t2 = new Thread(td,"b");

t1.start();
t2.start();



}

/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
accessStudent();
}

public void accessStudent() {

String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName+" is running!");
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread "+currentThreadName +" set age to:"+age);
Student student = getStudent();
student.setAge(age);
System.out.println("thread "+currentThreadName+" first read age is:"+student.getAge());
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread "+currentThreadName +" second read age is:"+student.getAge());

}

protected Student getStudent() {
Student student = (Student)studentLocal.get();
if(student == null) {
student = new Student();
studentLocal.set(student);
}
return student;
}

protected void setStudent(Student student) {
studentLocal.set(student);
}
}

运行程序后,屏幕输出:

b is running!

thread b set age to:0

thread b first read age is:0

a is running!

thread a set age to:17

thread a first read age is:17

thread b second read age is:0

thread a second read age is:17

可见,使用ThreadLocal后,我们不需要任何同步代码,却能够保证我们线程间数据的安全。

而且,ThreadLocal的使用也非常的简单。

我们仅仅需要使用它提供的两个方法

void set(Object obj) 设置当前线程的变量的副本的值。

Object get() 返回当前线程的变量副本

另外ThreadLocal还有一个protected的initialValue()方法。返回变量副本在当前线程的初始值。默认为null

ThreadLocal是怎么做到为每个线程都维护一个变量的副本的呢?

我们可以猜测到ThreadLocal的一个简单实现
public class ThreadLocal   
{
 private Map values = Collections.synchronizedMap(new HashMap());
 public Object get()
 {
  Thread curThread = Thread.currentThread();
  Object o = values.get(curThread);
  if (o == null && !values.containsKey(curThread))
  {
   o = initialValue();
   values.put(curThread, o);
  }
  return o;
 }

 public void set(Object newValue)
 {
  values.put(Thread.currentThread(), newValue);
 }

 public Object initialValue()
 {
  return null;
 }
}

由此可见,ThreadLocal通过一个Map来为每个线程都持有一个变量副本。这个map以当前线程为key。与synchronized相比,ThreadLocal是以空间换时间的策略来实现多线程程序。

Synchronized还是ThreadLocal?

ThreadLocal以空间换取时间,提供了一种非常简便的多线程实现方式。因为多个线程并发访问无需进行等待,所以使用 ThreadLocal会获得更大的性能。虽然使用ThreadLocal会带来更多的内存开销,但这点开销是微不足道的。因为保存在 ThreadLocal中的对象,通常都是比较小的对象。另外使用ThreadLocal不能使用原子类型,只能使用Object类型。 ThreadLocal的使用比synchronized要简单得多。

ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值