Java并发编程实战总结

线程安全性

  • 竞态条件:又有不恰当的执行顺序而出现的的不正确的结果
  • 当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程视图获得一个已经由它自己持有的锁,那么这个请求就会成功
  • 一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问
  • 当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁

对象的共享

  1. 非原子的64位操作:多线程程序中使用共享且可变的long和double等类型的变量是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来(分解为两个32位操作)

  2. volatile变量是一种比sychronized关键字更轻量级的同步机制

  3. 加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性

  4. 确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作

  5. 线程封闭(某个对象封闭在一个线程中)包括 Ad-hoc,栈封闭,ThreadLocal类

    1
    2
    3
    4
    5
    6
    7
    线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改
    只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象
    线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口进行访问而不需要进一步的同步
    保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象

对象的组合

  • 深拷贝与浅拷贝,举例说明:对象A1中包含对B1的引用,B1中包含对C1的引用。浅拷贝A1得到A2,
    A2 中依然包含对B1的引用,B1中依然包含对C1的引用。深拷贝则是对浅拷贝的递归,深拷贝A1得到A2,A2中包含对B2(B1的copy)的引用,B2 中包含对C2(C1的copy)的引用。

基础构建模块

  • 迭代器与ConcurrentModificationException,在迭代期间对容器加锁或者“克隆”容器,并在副本上进行迭代

  • 通过并发容器(ConcurrentHashMap等)来代替同步容器(synchronizedMap包装的),可以极大地提高伸缩性并降低风险

  • ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁(Lock Striping),在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。ConcurrentHashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能

  • CopyOnWriteArrayList 用于替代同步List,写入时复制(Copy-on-write)在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性

  • 阻塞队列(BlockingQueue,生产者-消费者模式)提供了可阻塞的put和take方法,以及支持定时的offer和poll方法(报错后可执行其他);类库中其他多种实现 LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,PriorityBlockingQueue是按优先级排序的队列,SynchronousQueue不会为队列中元素维护存储空间,直接交付工作,从而降低了将数据从生产者移动到消费者的延迟

  • 双端队列和工作密取:每个消费者都有各自的双端队列,如果一个消费者完成了自己双端队列中的全部工作,那么他可以从其他消费者双端队列末尾秘密地获取工作。工作密取非常适用于既是消费者也是生产者问题,当执行某个工作时可能导致出现更多的工作

  • 闭锁可以延迟线程的进度直到其到达终止状态,CountDownLatch是一种灵活的闭锁实现

  • FutureTask可以用做闭锁

  • 计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量

    信号量(Semaphore)的用途,1. 二值信号量,互斥体(mutex) 2. 实现资源池 3.有界阻塞容器

  • 栅栏与闭锁区别在于,所有线程必须同时到达栅栏位置,才能继续执行(闭锁是ABCD全部并行执行完毕后,E执行;栅栏是ABCD全部准备好后,一起并发执行)

任务执行

  1. 在生产环境中,为每个任务分配一个线程存在缺陷,避免无限制创建线程,尽量少使用newCachedThreadPool

  2. 线程池的创建,调用Executors中的静态工厂方法

    1
    2
    3
    4
    5
    6
    7
    newFixedThreadPool: 每当提交一个任务是就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(异常的,会补充线程)
    newCachedThreadPool:可缓存线程池,多了回收,少了添加,规模没有任何限制
    newSingleThreadExecutor:单线程的Executor,确保依照任务在队列中的顺序来串行执行
    newScheduledThreadPool:创建固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer
  3. 有关延迟任务与周期任务:Timer在执行所有定时任务时只会创建一个线程,如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时准确性,而newScheduledThreadPool线程池可以弥补这个缺陷,提供多个线程来执行

  4. Executor执行的任务有4个生命周期阶段:创建,提交,开始和完成。已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们能响应中断时,才能取消

  5. 为任务设置时效,在支持时间限制的Future.get中支持这种需求,当结果可用时,立即返回,如果在指定时限内没有计算出结果,将抛出TimeoutException

  6. Executor任务执行框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略

取消与关闭

  1. 线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的时候或者可能的情况下停止当前工作,并转而执行其他工作;通常,中断是实现取消的最合理方式
  2. 每个线程都有一个boolean类型的中断状态,在中断线程时,这个线程中断状态将被设置为true,列举Thread中有关
    1
    2
    3
    4
    5
    interrupt:中断目标线程
    isInterrupted: 返回目标线程的中断状态
    static interrupted:清除当前线程的中断状态,返回它之前的值

线程池的使用

  1. 使用ThreadLocal的任务:只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池的线程中不应该使用ThreadLocal在任务之间传递值

  2. 线程饥饿死锁:线程池中的任务需要无限期等待一些必须由池中其他任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁

  3. 设置线程池的大小:考虑部署的系统有多少个CPU;多大内存;任务是计算密集型;I/O密集型还是两者皆可;比如计算密集型任务,线程池大小为cpu数+1能实现最优,I/O密集型线程池的规模可以更大一些

  4. 若默认的执行策略,如newCachedThreadPool,newFixedThreadPool,newScheduledThreadExecutor 不能满足需求,可以通过ThreadPoolExecutor的构造函数来实例化一个对象

  5. 调用构造函数后再定制ThreadPoolExecutor,可以通过Setter来修改;如果Executor是通过Executors中的某个工厂方法创建的,那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器

  6. ThreadPoolExecutor是可扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute,afterExecute,terminated

  7. 如果需要提交一个任务集并等待它们完成,那么可以使用ExecutorService.invokeAll,并且在所有任务都执行完成后调用CompletionService来获取结果

    1
    2
    3
    4
    CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executor);
    //向线程池提交任务
    completionService.submit(new Runnable();
    completionService.take().get();

避免活跃性危险(有关死锁)

  1. 死锁的避免与诊断:支持定时的锁,通过JVM线程转储信息来分析死锁

  2. Thread API中定义的线程优先级只是作为线程调度的参考,其中定义了10个优先级,JVM根据需要将它们映射到操作系统的调度优先级,这种映射是于特定平台相关的

  3. 避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题,在大多数并发应用程序中,都可以使用默认的线程优先级

  4. 最常见的活跃性故障就是锁顺序死锁,在设计时应该避免产生锁顺序死锁,确保线程在获取多个锁时采用一致的顺序,最好的解决方法是在程序中始终使用开放调用(调用某个方法时不需要持有锁),这将大大减少需要同时持有多个锁的地方

性能和可伸缩性

1.可伸缩性指的是增加计算资源时(例如CPU,内存,存储容量,或I/O带宽),程序的吞吐量或者处理能力能相应地增加

2.在所有并发程序中都包含一些串行部分,如果你认为在程序中不存在串行部分,那么可以在仔细检查一遍
3.线程引入开销:

1
2
3
上下文切换:若可运行的线程数大于CPU的数量,那么操作系统会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU,导致一次上下文切换
内存同步:区分有竞争的同步和无竞争的同步非常重要
阻塞:当在锁上发生竞争时,竞争失败的线程肯定会阻塞

  1. 在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。三种方式可以降低锁的竞争程度

    1
    2
    3
    减少锁的持有时间 :缩小锁的范围
    降低锁的请求频率 :减小锁的粒度(锁分解和锁分段)
    使用带有协调机制的独占锁,这些机制允许更高的并发性,比如ReadWriteLock实现一种在多个读取操作以及单个写入操作情况下的加锁规则;原子变量
  2. 锁分段:锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段,例如ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列通的1/16

  3. 监测CPU的利用率,没有得到充分利用的原因一般包括:负载不充足,I/O密集,外部限制(数据库或web服务),锁竞争

并发程序测试

  1. 并发测试大致分为两类:安全性测试,活跃性测试(相关的是性能测试,包括吞吐量,响应性,可伸缩性)

  2. 总结:并发程序的许多故障模式都是一些低概率事件,它们对于执行时序,负载情况以及其他难以重现的条件都非常敏感,而且在测试程序中还会引入额外的同步或执行时序限制,用java编写的程序在测试起来更加困难,因为动态编译,垃圾回收以及自动优化等操作都会影响与时间相关的测试结果,因此我们需要将传统的测试技术与代码审查和自动化分析工具结合起来

显式锁

  1. 在Java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile,Java5.0增加了一种新的机制ReentrantLock是内置加锁机制不适用时的一种高级功能
  2. 使用ReentrantLock来保护对象状态,代码如下:必须在finally块中释放锁

    1
    2
    3
    4
    5
    6
    7
    8
    Lock lock = new ReentrantLock();
    lock.lock();
    try {
    System.out.println("更新对象状态");
    System.out.println("捕获异常,并在必要时恢复不变形条件");
    } finally {
    lock.unlock();
    }
  3. 与内置加锁机制不同的是,Lock提供了一种无条件的,可轮询的,定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的

  4. 在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized
  5. 有关ReadWriteLock读-写锁:允许多个读操作同时进行,但每次只允许一个写操作,ReentrantReadWriteLock为这两种锁都提供了可重入的加锁定义

构建自定义的同步工具

1.状态依赖性的管理:当前提条件未满足时,依赖状态的操作可以抛出一个异常或者返回一个错误状态(使其成为调用者的一个问题),也可以保持阻塞直到对象进入正确的状态

  1. 条件队列API中发出通知的方法,即notify和notifyAll。在调用notify时,JVM会从这个条件队列上等待的多个线程中选择一个来唤醒,而调用notifyAll则会唤醒所有在这个条件队列上等待的线程,并且发出通知的线程应该尽快地释放锁,从而确保正在等待的线程尽可能快地解除阻塞

  2. 只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll: 所有等待线程的类型都相同
    单进单出

  3. 有关显式的Condition对象:Lock比内置加锁提供了更为丰富的功能,Condition同样比内置条件队列提供了更丰富的功能,在每个锁上可存在多个等待,条件等待可以是可中断的或不可中断的,基于时限的等待,以及公平的或非公平的队列操作

  4. 在使用显式的Condition和内置条件队列之间进行选择时,与在ReentrantLock和synchronized之间进行选择是一样的,如果需要一些高级功能,例如使用公平的队列操作或者在每个锁上对应多个等待线程集,那么应该优先使用Condition而不是内置条件队列

  5. 有关AbstractQueuedSynchronizer(AQS):是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来,包括ReentrantLock,Semaphore,CountDownLatch,ReetrantReadWriteLock,SynchronousQueue,FutureTask

  6. 小结:有时候现有的库类不能提供足够的功能,在这种情况下,可以使用内置的条件队列,显式的Condition对象或者AbstractQueuedSynchronizer来构建自己的同步器

原子变量与非阻塞同步机制

1.硬件对并发的支持:比较并交换(CAS),Java5.0后引入底层的支持,原子变量中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时则直接或间接地使用了这些原子变量类

2.原子变量是一种“更好的volatile”:

1
2
3
4
5
6
7
8
9
10
11
有关volatile:
1. 不要将volatile用在getAndOperate场合,仅仅set或者get的场景是适合volatile的
2. 内存屏蔽的概念,是一个CPU指令,告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行,另一个作用是强制更新一次不同CPU的缓存
3. volatile与内存屏蔽的关系:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,但中间的步骤是不安全的,非原子性
4. volatile保证对线程的可见性,但不保证原子性
原子变量具有原子性和可变性是因为使用到CAS指令,结合volatile

  1. 在中低程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能够更有效地避免竞争。但实际情况中,原子变量在可伸缩性上要高于锁

  2. 构建非阻塞算法的技巧在于:将执行原子修改的范围缩小到单个变量上

  3. 非阻塞算法通过底层的并发原语(CAS)来维持线程的安全性,这些底层的原语通过原子变量类向外公开。

java内存模型

  1. Java并发注解
  • 类注解(@Immutable,@ThreadSafe,@NotThreadSafe)只是说明作用
  • 域注解
  • 方法注解(@GuardedBy(lock)这个方法被哪个锁保护着)
  1. 有关静态域:静态初始化期间,内存写入操作将自动对所有线程可见,因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步

  2. 小结:Java内存模型说明了某个线程的内存操作在哪些情况下对于其他线程是可见的,其中包括确保这些操作是按照一种Happens-Before的偏序关系进行排序,而这种关系是基于内存操作和同步操作等级别来定义的,如果缺少充足的同步,那么当线程访问共享数据时,会发生一些非常奇怪的问题,然而使用@GuardedBy和安全发布,即使不考虑Happens-Before底层细节,也能确保线程安全性

线程池的使用

线程池

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

重要的类

1
2
3
4
ExecutorServcie //真正的线程池接口。
ScheduledExecutorService //能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor //ExecutorService的默认实现。
ScheduledThreadPoolExecutor //继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现

线程池工具类

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
1. newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4. newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

设计模式之观察者模式

定义

在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都全部受到通知并自动更新。

要点

  • 使用此模式时,你可从被观察者处推(push)或拉(pull)数据。
  • 有多个观察者时,不可以依赖特定的通知次序。
  • Java有多个观察者模式的实现,包括了通用的java.util.Observable,要注意这种实现带来的一些问题

并发编程之多线程

线程安全

概念

1
2
3
1、当多个线程访问访问某一个类(对象或方法)时,这个类或对象或方法始终能表现出正确的行为或我们想要的结果,那么这个类(对象或方法)
就是线程安全的。
2、synchronized:可以在任意的对象及方法上加锁,而加锁的这段代码称之为互斥区或者临界区。

线程和锁

多个线程多个锁:多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。

代码示例

两个线程t1,t2分别依次start,访问两个对象的synchronized修饰的printNum方法,Code如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
* 所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock),
*
* 在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)。
* @author xujin
*
*/
public class MultiThread {
private int num = 0;
public synchronized void printNum(String tag) {
try {
if (tag.equals("a")) {
num = 100;
System.out.println("tag a, set num over!");
Thread.sleep(1000);
} else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 注意观察run方法输出顺序
public static void main(String[] args) {
// 两个不同的对象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}

执行结果如下:

1
2
3
4
tag a, set num over!
tag b, set num over!
tag b, num = 200
tag a, num = 100

关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock)

如果,在静态方法上printNum()加一个synchronized关键字修饰的话,那这个线程调用printNum()获得锁,就是这个类级别的锁。这是时候无论你实例化出多少个对象m1,m2都是没有任何关系的。
这样测试的结果是

1
2
3
4
tag a, set num over!
tag a, num = 100
tag b, set num over!
tag b, num = 200

小结:
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁,所以示例中代码中的哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法对象的锁,也就是Lock,两个对象,线程获得的就是两个不同的锁,他们互不影响。
有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁独占.class类。

脏读

业务整体需要使用完整的synchronized,保持业务的原子性。

在我们对对象中的一个方法加锁的时候,需要考虑业务的或程序的整体性,也就是为程序中的set和get方法同时加锁synchronized同步关键字,保证业务的(service层)的原子性,不然会出现数据错误,脏读。

synchronized的重入

什么是synchronized的重入锁

synchronized,它拥有强制原子性的内置锁机制,是一个重入锁,所以在使用synchronized时,当一个线程请求得到一个对象锁后再次请求此对象锁,可以再次得到该对象锁,就是说在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以拿到锁。
当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞

简单的说:关键字synchronized具有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁的锁后,再次请求此对象时可以再次得到该对象对应的锁。

这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething时,该线程获得child对象的锁,在doSomething方法内执行doAnotherThing时再次请求child对象的锁,因为synchronized是重入锁,所以可以得到该锁,继续在doAnotherThing里执行父类的doSomething方法时第三次请求child对象的锁,同理可得到,如果不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而导致死锁。
所以在Java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。因为java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程获得对象锁的操作是以每线程为粒度的,per-invocation互斥体获得对象锁的操作是以每调用作为粒度的)

其实现方法是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

注意就是不要使用String的常量加锁,会出现死循环问题。

锁对象的改变问题:

当使用一个对象进行加锁的时候,要注意对象本身发生变化的时候,那么持有的锁就不同。如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了变化。

同一对象属性的修改不会影响锁的情况

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ModifyLock {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public synchronized void changeAttributte(String name, int age) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 开始");
this.setName(name);
this.setAge(age);
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 修改对象内容为: " + this.getName() + ", "
+ this.getAge());
Thread.sleep(2000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final ModifyLock modifyLock = new ModifyLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("许进", 25);
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("李四X", 21);
}
}, "t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}

运行结果:

1
2
3
4
5
6
当前线程 : t1 开始
当前线程 : t1 修改对象内容为: 许进, 25
当前线程 : t1 结束
当前线程 : t2 开始
当前线程 : t2 修改对象内容为: 李四X, 21
当前线程 : t2 结束