线程安全的几种实现方式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全的几种实现方式相关的知识,希望对你有一定的参考价值。

参考技术A 1,用final 修饰,不能修改的变量就是最安全的

2,用原子类或者线程安全的类,比如int的原子包装类的增长就是通过自旋和cas实现的,又比如concurrenthashmap

3,用阻塞的方式,比如同步块等

4,用非阻塞的方法,比如aqs实现,乐观锁等

5,采用线程封闭,localthread

线程安全的概念,实现线程安全的几种方法

对于多线程编程,很多人概念不清,写代码的时候要么是处处加锁,影响性能不说,还容易莫名其妙的死锁,还有人对多线程敬而远之。

C/C++:
1.局部变量局部使用是安全的
为什么?因为每个thread 都有自己的运行堆栈,而局部变量是生存在堆栈中,大家不干扰。
所以代码1
int local1;
++local1;
是安全的

2.全局原生变量多线程读写是不安全的
全局变量是在堆(heap)中
long global1 = 0;
++global2;
++这个操作其实分为两部,一个是读,另外一个是写
mov ecx,global
add ecx,1
mov global,ecx
所以代码3处是不安全的

3.函数静态变量多线程读写也是不安全的
道理同2
所以代码2处也是不安全的

4.volatile能保证全局整形变量是多线程安全的么
不能。
volatile仅仅是告诫compiler不要对这个变量作优化,每次都要从memory取数值,而不是从register
所以代码4也不是安全

5.InterlockedIncrement保证整型变量自增的原子性
所以代码5是安全的

6.function static object的初始化是多线程安全的么
不是。
著名的Meyer Singleton其实不是线程安全的
Object & getInstance()

static Object o;
return o;

可能会造成多次初始化对象
所以代码6处是不安全的

7.在32机器上,4字节整形一次assign是原子的
比如
i =10; //thread1
i=4; //thread2
不会导致i的值处于未知状态,要么是10要么是4

写好多线程安全的法宝就是封装,使数据有保护的被访问到
安全性:
局部变量>成员变量>全局变量
原文
JAVA:
并发性

只有当要解决的问题需要一定程度的并发性时,程序员才会从多线程应用程序中受益。例如,如果打印队列应用程序仅支持一台打印机和一台客户机,则不应该将它编写为多线程的。一般说来,包含并发性的编码问题通常都包含一些可以并发执行的操作,同时也包含一些不可并发执行的操作。例如,为多个客户机和一个打印机提供服务的打印队列可以支持对打印的并发请求,但向打印机的输出必须是串行形式的。多线程实现还可以改善交互式应用程序的响应时间。

Synchronized 关键字

虽然多线程应用程序中的大多数操作都可以并行进行,但也有某些操作(如更新全局标志或处理共享文件)不能并行进行。在这些情况下,必须获得一个锁来防止其他线程在执行此操作的线程完成之前访问同一个方法。在 Java 程序中,这个锁是通过 synchronized 关键字提供的。清单 1 说明了它的用法。

清单 1. 使用 synchronized 关键字来获取锁
public class MaxScore
int max;
public MaxScore()
max = 0;

public synchronized void currentScore(int s)
if(s> max)
max = s;


public int max()
return max;

这里,两个线程不能同时调用 currentScore() 方法;当一个线程工作时,另一个线程必须阻塞。但是,可以有任意数量的线程同时通过 max() 方法访问最大值,因为 max() 不是同步方法,因此它与锁定无关。

试考虑在 MaxScore 类中添加另一个方法的影响,该方法的实现如清单 2 所示。

清单 2. 添加另一个方法
public synchronized void reset()
max = 0;

这个方法(当被访问时)不仅将阻塞 reset() 方法的其他调用,而且也将阻塞 MaxScore 类的同一个实例中的 currentScore() 方法,因为这两个方法都访问同一个锁。如果两个方法必须不彼此阻塞,则程序员必须在更低的级别使用同步。清单 3 是另一种情况,其中两个同步的方法可能需要彼此独立。

清单 3. 两个独立的同步方法
import java.util.*;
public class Jury
Vector members;
Vector alternates;
public Jury()
members = new Vector(12, 1);
alternates = new Vector(12, 1);

public synchronized void addMember(String name)
members.add(name);

public synchronized void addAlt(String name)
alternates.add(name);

public synchronized Vector all()
Vector retval = new Vector(members);
retval.addAll(alternates);
return retval;

此处,两个不同的线程可以将 members 和 alternates 添加到 Jury 对象中。请记住, synchronized 关键字既可用于方法,更一般地,也可用于任何代码块。清单 4 中的两段代码是等效的。

清单 4. 等效的代码
synchronized void f() void f()
// 执行某些操作 synchronized(this)
// 执行某些操作

所以,为了确保 addMember() 和 addAlt() 方法不彼此阻塞,可按清单 5 所示重写 Jury 类。

清单 5. 重写后的 Jury 类
import java.util.*;
public class Jury
Vector members;
Vector alternates;
public Jury()
members = new Vector(12, 1);
alternates = new Vector(12, 1);

public void addMember(String name)
synchronized(members)
members.add(name);


public void addAlt(String name)
synchronized(alternates)
alternates.add(name);


public Vector all()
Vector retval;
synchronized(members)
retval = new Vector(members);

synchronized(alternates)
retval.addAll(alternates);

return retval;

请注意,我们还必须修改 all() 方法,因为对 Jury 对象同步已没有意义。在改写后的版本中,addMember()、addAlt() 和 all() 方法只访问与 members 和 alternates 对象相关的锁,因此锁定 Jury 对象毫无用处。另请注意,all() 方法本来可以写为清单 6 所示的形式。

清单 6. 将 members 和 alternates 用作同步的对象
public Vector all()
synchronized(members)
synchronized(alternates)
Vector retval;
retval = new Vector(members);
retval.addAll(alternates);


return retval;

但是,因为我们早在需要之前就获得 members 和 alternates 的锁,所以这效率不高。清单 5 中的改写形式是一个较好的示例,因为它只在最短的时间内持有锁,并且每次只获得一个锁。这样就完全避免了当以后增加代码时可能产生的潜在死锁问题。

同步方法的分解

正如在前面看到的那样,同步方法获取对象的一个锁。如果该方法由不同的线程频繁调用,则此方法将成为瓶颈,因为它会对并行性造成限制,从而会对效率造成限制。这样,作为一个一般的原则,应该尽可能地少用同步方法。尽管有这个原则,但有时一个方法可能需要完成需要锁定一个对象几项任务,同时还要完成相当耗时的其他任务。在这些情况下,可使用一个动态的“锁定-释放-锁定-释放”方法。例如,清单 7 和清单 8 显示了可按这种方式变换的代码。

清单 7. 最初的低效率代码
public synchonized void doWork()
unsafe1();
write_file();
unsafe2();

清单 8. 重写后效率较高的代码
public void doWork()
synchonized(this)
unsafe1();

write_file();
synchonized(this)
unsafe2();

清单 7 和清单 8 假定第一个和第三个方法需要对象被锁定,而更耗时的 write_file() 方法不需要对象被锁定。如您所见,重写此方法以后,对此对象的锁在第一个方法完成以后被释放,然后在第三个方法需要时重新获得。这样,当 write_file() 方法执行时,等待此对象的锁的任何其他方法仍然可以运行。将同步方法分解为这种混合代码可以明显改善性能。但是,您需要注意不要在这种代码中引入逻辑错误。

嵌套类

内部类在 Java 程序中实现了一个令人关注的概念,它允许将整个类嵌套在另一个类中。嵌套类作为包含它的类的一个成员变量。如果定期被调用的的一个特定方法需要一个类,就可以构造一个嵌套类,此嵌套类的唯一任务就是定期调用所需的方法。这消除了对程序的其他部分的相依性,并使代码进一步模块化。清单 9,一个图形时钟的基础,使用了内部类。

清单 9. 图形时钟示例
public class Clock
protected class Refresher extends Thread
int refreshTime;
public Refresher(int x)
super(“Refresher”);
refreshTime = x;

public void run()
while(true)
try
sleep(refreshTime);

catch(Exception e)
repaint();



public Clock()
Refresher r = new Refresher(1000);
r.start();

private void repaint()
// 获取时间的系统调用
// 重绘时钟指针

清单 9 中的代码示例不靠任何其他代码来调用 repaint() 方法。这样,将一个时钟并入一个较大的用户界面就相当简单。

事件驱动处理

当应用程序需要对事件或条件(内部的和外部的)作出反映时,有两种方法或用来设计系统。在第一种方法(称为轮询)中,系统定期确定这一状态并据此作出反映。这种方法(虽然简单)也效率不高,因为您始终无法预知何时需要调用它。

第二种方法(称为事件驱动处理)效率较高,但实现起来也较为复杂。在事件驱动处理的情况下,需要一种发信机制来控制某一特定线程何时应该运行。在 Java 程序中,您可以使用 wait()、notify() 和 notifyAll() 方法向线程发送信号。这些方法允许线程在一个对象上阻塞,直到所需的条件得到满足为止,然后再次开始运行。这种设计减少了 CPU 占用,因为线程在阻塞时不消耗执行时间,并且可在 notify() 方法被调用时立即唤醒。与轮询相比,事件驱动方法可以提供更短的响应时间。

创建高效的线程安全类的步骤

编写线程安全类的最简单的方法是用 synchronized 声明每个方法。虽然这种方案可以消除数据损坏,但它同时也会消除您预期从多线程获得的任何收益。这样,您就需要分析并确保在 synchronized 块内部仅占用最少的执行时间。您必须格外关注访问缓慢资源 ― 文件、目录、网络套接字和数据库 ― 的方法,这些方法可能降低您的程序的效率。尽量将对这类资源的访问放在一个单独的线程中,最好在任何 synchronized 代码之外。

一个线程安全类的示例 被设计为要处理的文件的中心储存库。它与使用 getWork() 和 finishWork() 与 WorkTable 类对接的一组线程一起工作。本例旨在让您体验一下全功能的线程安全类,该类使用了 helper 线程和混合同步。请注意继续添加要处理的新文件的Refresher helper 线程的用法。本例没有调整到最佳性能,很明显有许多地方可以改写以改善性能,比如将 Refresher 线程改为使用 wait()/notify() 方法事件驱动的,改写 populateTable() 方法以减少列出磁盘上的文件(这是高成本的操作)所产生的影响。
原文

以上是关于线程安全的几种实现方式的主要内容,如果未能解决你的问题,请参考以下文章

单例模式的几种实现方式

线程安全的概念,实现线程安全的几种方法

Java线程安全的单例模式的几种实现

线程安全的单例模式的几种实现

Android中的几种多线程实现

单例模式