【Java并发编程实践】— 线程安全

前言:

如果没有明确的同步来管理共享数据,一个线程可能会修改其他线程正在使用的数据,产生意外的结果;

编写线程安全的代码,本质上就是管理对状态的访问,而且通常是共享的、可变的状态。

状态:通俗的说,一个对象的状态就是它的数据,存储在状态变量中,比如实例属性或静态属性。

共享:是指一个变量可以被多个线程访问;

可变:是指变量的值在其生命周期内可以改变;


无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步机制来协调线程对该变量的访问。Java提供了synchronized关键字,对具体一个对象实现线程独占,完成所谓的原子操作。


一.线程安全

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是 “线程安全” 的。


二.原子性

1.何谓Atomic

Atomic一词跟原子有点关系,曾被认为是不能被进一步分割的最小粒子,计算机中的Atomic是指不能分割成若干部分的意思。如果一段代码被认为是Atomic,则表示这段代码在执行过程中,是不能被中断的。


2.原子操作:

原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断;

原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。


3.提供原子操作的类:

java.util.concurrent.atomic包中提供了许多关于原子操作类。

public class Test implements Runnable {
	// 创建具有初始值 0 的 AtomicInteger
	private AtomicInteger atInteger = new AtomicInteger();

	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {

			/*
			 * 以原子方式将当前值加 1; 也就是说当一个线程执行原子操作时,是不能被中断的,直到执行完毕.所以该方式是线程安全的.
			 */
			atInteger.getAndIncrement();

			System.out.println(Thread.currentThread().getName() + "..."
					+ atInteger.get());
			try {
				Thread.sleep(1000L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

		}
		System.out.println(Thread.currentThread().getName() + ":" + atInteger.get());
	}

	public static void main(String[] args) {
		Test test = new Test();
		Thread th1 = new Thread(test);
		Thread th2 = new Thread(test);
		th1.start();
		th2.start();
	}
     
}

三.锁

1.内部锁

Java提供了强制原子性的内置锁机制:

synchronized块, 一个synchronized块有两部分:锁对象的引用,以及这个锁保护的代码块;

synchronized方法是对跨越了整个方法体的synchronized块的简短描述,至于synchronized方法的锁,就是该方法所在对象本身。(静态的synchronized方法从Class对象上获取锁)。

语法:

     synchronized(lock){

           //访问或修改被锁保护的共享状态

     }


执行线程进入synchronized块之前会自动获得锁,无论通过正常控制路径退出,还是从块中抛出异常,线程都在放弃对synchronized块的控制时自动释放锁。获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。


内部锁在Java中扮演了互斥锁的角色,意味着最多只有一个线程可以拥有该锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它。如果B永远不释放锁,A将永远等下去。


同一时间,只能有一个线程可以运行特定锁保护的代码块,因此,由同一个锁保护的synchronized块会各自原子地执行,不会相互干扰。


注意:原子性的含义与它在事务性应用中相同——   一组语句作为单独的,不可分割的单元运行。


2.重进入
当一个线程请求其它线程已经占有的锁时,请求线程将被阻塞。然而内部锁是可重进入的。因此线程在试图获得它自己占有的锁时,请求会成功。重进入意味着所有请求是基于“每个线程”,而不是基于“每次调用”的。

public class ReEntry implements Runnable
{
    public synchronized void firstMth()
    {
        /*
         * 验证内部锁是否可重入:
         * firstMth()与lastMth()都是synchronized类型的,都会在处理前试图获得ReEntry的锁,
         * 如果内部锁不是可重入的,lastMth()的调用者就永远无法得到ReEntry的锁,因为锁已经被占有,
         * 导致线程永远地延迟,等待一个永远无法获得的锁。 —— 重进入帮助我们避免了这种死锁
         */
        lastMth();
        System.out.println("enter firstMth...");
    }

    public synchronized void lastMth()
    {
        System.out.println("enter LastMth...");
    }

    @Override
    public void run()
    {
        firstMth();
    }

    public static void main(String[] args)
    {
        Thread th = new Thread(new ReEntry());
        th.start();
    }
}

重进入的实现是为每一个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,计数递增;每次占有线程退出同步块,计数器值将递减。直到计数器达到0时,锁被释放。

3.用锁保护状态
一种常见的锁规则是在对象内部封装所有可变状态,通过对象的内部锁来同步任何访问可变状态的代码路径,保护它在并发访问中的安全。很多线程安全类都是这个模式。

例如java.util.Vector类。这种情况下,对象状态中的一切变量都被对象内部锁保护。如果添加新的代码路方法或代码路径而忘记使用锁,这种锁协议也很容易被破坏。

并不是所有的数据都需要锁保护——只有那些被多个线程访问的可变数据。

4.活跃度与性能
场景:
现在假设我们把Servlet中的service()声明为synchronized,即同步方法。因此每次只能有一个线程执行它。这违背了Servlet框架的使用初衷——Servlet可以同时处理多个请求——并且当负载过高时会引起客户的不满。如果Servlet正忙于处理一个复杂运算,那么在它可以处理一个新的请求开始前,其他用户必须等待,直到当前请求完成。在多CPU系统中,即时负载很高,仍然会有处理器处于空闲。

以上这种web应用的运行方式描述为“弱并发”的一种表现:限制并发调用数量的,并非是可用的处理器资源,而恰恰是应用程序自身的结构。

通过缩小synchronized块的范围来维护线程安全性,可以很容易提升Servlet的并发性。

注意:应谨慎的控制synchronized块,不可以将一个原子操作分解到多个synchronized块中。不过你应该尽量从synchronized块中分离出耗时的且不影响共享状态的操作。—— 这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。


四. 线程安全例子

public class NotThreadSafe implements Runnable
{
    
    private int value = 0;
    
    @Override
    public  void run()
    {
         System.out.println("before——"+Thread.currentThread().getName()+"——"+value);
         try
        {
            Thread.sleep(100L);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        value++;
        System.out.println("before——"+Thread.currentThread().getName()+"——"+value);
    }
    
    public static void main(String[] args) throws InterruptedException
    {
        NotThreadSafe notSafe = new NotThreadSafe();
        Thread[] ths = new Thread[1000];
        
        /*
         * 非线程安全的自增操作:
         * 在没有同步的情况下,创建1000个线程执行自增操作value++,很有可能会遗失更新,使统计结果不准确
         */
        for (int i = 0; i < 1000; i++)
        {
            ths[i]=new Thread(notSafe);
            ths[i].start();
        }
        for (int i = 0; i < ths.length; i++)
        {
            ths[i].join();
        }
        System.out.println("统计结果:"+notSafe.value);
    }
    
}
运行结果:996(理想情况下应该是1000,所以这里遗失更新了)

分析:

假如value的初始值为9,现在有两个线程(A、B)执行该自增操作,线程A、B可能会交替运行(也就是并发),所以线程A、B可能会同时读取到value的值为9,然后同时加1,最后都将value的值设置为10(理想的结果应为:11,可是现在结果为:10)这就会造成其中一次自增操作遗失了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值