线程安全问题——线程冲突、死锁

什么是线程安全
线程安全就是多线程访问同一代码,不会产生不确定的结果。

如何保证线程安全

  • 对非安全的代码进行加锁控制;
  • 多线程并发情况下,线程共享的变量改为方法级的局部变量。

线程安全问题

线程冲突、死锁等问题都属于线程安全问题,具体的:两个线程对一个变量进行操作,但是没有上锁,即没有进行同步操作,就像买车票的时候多个窗口一起卖,但是车票数没有减去。车票被重复卖出。就是不安全。还有,被多个线程操作的变量改为静态量加static,才安全。

线程安全很简单,就是指一个函数(方法、属性、字段或者别的)在同一时间被不同线程使用,不会造成任何线程冲突的问题。就说这个东西是线程安全的。

一个不能被改变的资源是线程安全的,比如说一个常量:

const double pai = 3.14159265;

因为pai的值不可能被改变,所以在不同的线程中使用也不会造成冲突。换言之它在不同的线程中同时被使用和在一个线程中被使用是没有区别的,所以这个东西是线程安全的。

线程冲突

当两个运行在不同线程的操作,作用在同一个数据上,会发生线程冲突 (Thread interference)。

这也意味着,两个操作分别由多个步骤组成,且两个操作同时执行,会导致步骤交叠。

线程冲突的必要条件是多线程共享资源

在java中,对于多线程冲突问题,我们采用一种常规的解决方法,就是同步机制,即对操作加synchronized关键字。
synchronized 方法控制对类成员变量的访问: 每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞 ,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。

如果一个函数里面没有使用任何可能共享的资源,那么就不可能出现线程冲突,也就是线程安全的。比如说这样的函数:

static int Add( int a, int b )
{
	return a + b;
}

这个函数中所使用的所有的资源都是自己的局部变量,而函数的局部变量是储存在堆栈上的,每个线程都有自己独立的堆栈,所以局部变量不可能跨线程共享。所以这样的函数显然是线程安全的。

但值得注意的是:下面的函数不是线程安全的:

void Swap( int& a, int& b )
{
	int c = a;
	a = b;
	b = c;
}

因为函数的参数是按引用传递进来的,换言之a和b看起来是函数的局部变量,但实际上却是函数外面的东西,如果这两个东西是另一个函数的局部变量,倒也没有问题,如果这两个东西是全局变量(静态成员),就不能确保没有线程冲突了。而在上个例子中,a和b在传入函数之时,就做了一个拷贝的动作,所以传进来的a、b到底是全局变量还是静态成员都没有关系了。

但是,如果一个函数只调用线程安全的函数,只使用线程安全的共享资源,那么这个函数也是线程安全的。

Java中多线程访问冲突的解决方式

当时用多线程访问同一个资源时,非常容易出现线程安全的问题,例如当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失。因此需要采用同步机制来解决这种问题。

  1. 同步方法(synchronized关键字修饰的方法)、同步代码块(synchronized关键字修饰的语句块)
    当使用synchronized来修饰某个共享资源的时候,如果线程Thread01在执行synchronized代码,另外一个线程Thread02也要同时执行同一对象的统一synchronized代码时,线程Thread02将要等到线程Thread01执行成后才能继续执行。在这种情况下,可以使用wait()方法和notify()方法。
    在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并且可以调用notify()方法或者notifyAll()方法通知正在等待的而其他线程,notify()唤醒一个线程(等待队列中的第一个线程),并允许它去获得锁,而notifyAll()方法唤醒所有等待这个对象的线程,并允许它们去竞争获得锁。

  2. 使用特殊成员变量(volatile 成员变量)实现线程同步(前提是对成员变量的操作是原子操作)
    volatile是一个类型修饰符,被设计用来修饰被不同线程访问和修饰的变量。当变量没有被volatile修饰时,线程读取数据时可能会从缓存中去读取,如果其他线程修改了该变量,则无法读取到修改后的数据。volatile关键字相当于告诉虚拟机该成员变量可能会被其他线程修改。当变量被volatile修饰时,线程每次使用时都会直接到内存中提取,而不会利用缓存,从而保证了数据的同步,获得的数据是最新被修改的数据。
    但是volatile不能保证操作的原子性,一般不能替代synchronized代码块,除非对变量的操作是原子操作的情况下才可以使用volatile。

  3. 使用Lock接口(java.util.concurrent.locks包)

  4. 使用线程局部变量(thread-local)解决多线程对同一变量的访问冲突,而不能实现同步(ThreadLocal类)
    如果使用ThreadLocal来管理变量,则每一个使用该变量的线程都会获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。所以对于同线程对共享变量的操作互不影响。
    Thread-local与同步机制的比较:
    1)两者都是为了解决多线程中相同变量的访问冲突问题
    2)Thread-local采用“空间换时间”方法,同步机制采用“时间换空间”的方式

  5. 使用阻塞队列实现线程同步(java.util.concurrent包)

  6. 使用原子变量实现线程同步 (java.util.concurrent.atomic包)
    需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
    原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即这几步要么同时完成,要么都不完成。

线程间的同步方式

线程同步: 指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!

  • 临界区: 通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。它并不是核心对象,不是属于操作系统维护的,而是属于进程维护的。

  • 互斥量(Mutex): 内核对象,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。

  • 信号量(Semphares) : 内核对象,它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。

  • 事件(Event) : 内核对象,Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值