java 并发编程知识(一)

在构建稳定的并发程序时,必须正确地使用线程和锁,但这些终究是一些机制,要编写线程安全的代码,其核心在于要对状态访问操作进程管理,特别是共享的和可变的状态访问。

’共享‘意味着变量可以由多个线程同时访问,而‘可变’则意味着变量的值在其生命周期内可以发生变化。

一个对象是否是线程安全的,取决于它是否被多个线程访问,这指的是程序中访问的方式,而不是对象要实现的方式。要使得对象线程安全,需要采用同步机制来协同对对象可变状态的访问,如果无法实现协同,那么可能导致数据破坏或出现意料之外的结果。

当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问,java中同步机制关键字是Synchronized,它提供了一种独占的加锁方式,但同步这个术语还包括volatile类型的变量,显式(Explicli lock)锁以及原子变量

当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误,有三种方式可以修复这个问题
1.不在线程之间共享该状态变量
2.将状态变量修改为不可变的变量
3.在访问状态变量时使用同步
当设计线程安全的类时,良好的面向对象技术、不可修改性、以及明晰的规范,都能起到一定的帮助作用

线程安全类指:多个线程访问某个类时,不管运行时环境采用何种调度方式或者线程无论如何交替执行,并且在主调代码中不需要任何的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全类

在线程安全类中封装了必要的同步机制,因此客户端无需进一步采用同步措施

无状态对象一定是线程安全的,

原子性:假设有两个操作,A,B ,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子性的,原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作以原子方式执行的操作

在实际情况中,应尽可能的使用现有的线程安全对象,来管理类的状态,与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性

要保证状态的一致性,就需要在单个原子操作中,更新所有相关的状态变量。

扫描二维码关注公众号,回复: 9456718 查看本文章

java提供了一种内置锁机制来支持原子性,同步代码块 (Synchronized block)它包括两个部分,一个作为锁的对象使用,一个作为由这个锁保护的代码块,以synchronized关键字修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象,静态的synchronized方法以calss对象为锁,

```
synchronized(lock){
访问或修改由锁保护的共享状态
}
```

每个java对象都可以用做一个实现同步的锁,这些锁被称为内置锁监视器锁,线程在进入同步代码块之前会自动获取锁,并且在退出时释放锁,而无论通过正常的控制路径推出还是通过代码块内抛出异常退出,获取内置锁的唯一途径,就是进入由这个锁保护的同步代码块或方法。

java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有锁,当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直至B释放,如果B不释放,A将永远等待下去。 由于每次只能有一个线程执行内置锁保护的代码块,因此代码块会以原子操作执行,多个线程在执行时也不会互相干扰

当某个线程请求一个,其他线程持有的锁时,发出请求的线程就会阻塞,然而由于内置锁是可重入的,因此某个线程试图获得一个由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作力度是线程,而不是调用

重入的一种实现方式是,为每个锁关联一个获取计数器和一个所有者线程,当计数器为0时,就认为这个锁没有被任何线程持有,当线程请求一个未被持有的锁,jvm将记录下锁的持有者,并且获取计数器置为1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块,计数器会相应递减,当计数器为0,这个锁将被释放,

重入进一步提升了加锁行为的封装性,也避免了死锁情况的发送,因此简化了面向对象并发代码的开发,

如果不可重入,子类继承父类重写父类的由sync关键字修饰的方法,那么在子类在调用父类方法执行的时候会死锁,一致堵塞,因为锁已经被持有,无法在获取锁

用锁保护状态:对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的

每个共享的和可变的变量都应该只有一个锁来保护,从而是维护人员知道时哪一个锁
一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问,在许多线程安全类中都使用了这种模式,例如vector,和其他同步集合类

synchronized关键字也不要滥用,过多的同步使得代码执行性能非常糟糕

通常在简单性和性能之间存在相互制约因素,当实现某个同步策略时,一定不要盲目的为了性能,而牺牲简单些

当执行时间较长的计算或无法快速完成的操作时,一定不要持有锁

在没有同步的情况下,编译器,处理器,以及运行时都可能对操作的执行顺序进行一些意想不到的调整,在缺乏足够的同步的多线程程序中,要相对内存操作的执行顺序进行判断,几乎无法得出正确的结论

加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读操作,或写操作必须在同一个线程锁上同步

java提供了一种稍弱的同步机制,即volatile变量,它是比synchronized关键字更轻量级的同步机制,为了确保将变量的更新操作通知到其他线程,当把变量声明到volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量的操作与其他内存操作一起重排序,

volatile变量不会被缓存在寄存器或其他处理器看不见的地方,因此在读取volatile类型的变量时总会返回最新的值,在访问volatile类型变量时不会执行枷锁操作,因此也不会导致线程阻塞,所以才会说它是比synchronized关键字更轻量级的同步机制
仅当volatile变量能简化代码的实现以及同步策略的验证时,才应该使用他们,如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量,正确使用方式是:确保它们自身状态的可见性,确保他们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生
例:

		volatile boolean asleep;
		while (!asleep){
		sout...
		}		

加锁机制既能保证原子性又可以确保可见性,而volatile变量只能确保可见性
当且仅当满足以下条件才使用volatile变量:

  • 对变量的写入操作,不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
  • 该变量不会与其他状态变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

未完待续。。。

发布了21 篇原创文章 · 获赞 1 · 访问量 383

猜你喜欢

转载自blog.csdn.net/qq_44909430/article/details/103696108
今日推荐