为什么会出现线程不安全?
多个线程之间是不能相互传递数据通信的,他们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了jvm的主内存,住内存是多个线程共享的。当new一个对象的时候,也是被分配到主内存中,每个线程都有自己的工作内存,工作内存存储了主内存的某些对象副本,当线程操作某个对象时,执行顺序如下:
1、从主存复制变量到当前的工作内存
2、执行代码,改变共享变量
3、用工作内存数据刷新主存相关内容
线程引用变量的时候不能直接从主存中引用,如果线程工作内存中没有该变量,则会从主存中拷贝一个变量副本到工作内存中。这个过程为read-load,完成后线程会引用该副本。当同一线程在读引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本(user),这时候线程与线程之间的操作的先后顺序,就会决定了你的程序对主内存区最后的修改是不是正确的。也称之为时序性问题
当多个线程操作一个数据结构时,产生了相互修改的情况,没有保证数据的一致性。我们通常称这种设计为线程不安全的。
示例(线程不安全)
**场景:**5个线程给数字做加1操作,
程序:在线程得到数字之后我做了打印结果操作,+1操作之后,再次打印结果
期望结果:加1操作的结果真的是初始结果做了+1,可能有点绕,例如线程1开始拿到的i=1,那么做了i=i+1之后,他最终得到的结果应该是i=2,这样才能保证中间过程没有收到干扰;
多说无益,上代码
/**
* 线程同步测试
* @author a
*
*/
public class SynchronizedTest {
int i =0;
public void increamI() throws InterruptedException{
System.out.println(Thread.currentThread().getName()+"--修改前---"+i);
i=i+1;
System.out.println(Thread.currentThread().getName()+"--修改后---"+i);
}
}
public class SyncRun {
public static void main(String[] args) {
final SynchronizedTest st = new SynchronizedTest();
for(int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
st.increamI();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
}
}
以下是我的到的结果集
大家可以看到,他的结果是毫无规律可循,杂乱无章的,因为各个线程之间相互影响
场景2:两个线程分别打印一串字符,期望能一直得到字符的完整性。
代码如下:
public class SyncRun {
public static void main(String[] args) {
final SynchronizedTest st = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
// TODO Auto-generated method stub
st.testSync("xiancheng1-------------");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
// TODO Auto-generated method stub
st.testSync("thread2**************");
}
}
}).start();
}
}
/**
* 线程同步测试
* @author a
*
*/
public class SynchronizedTest {
public void testSync(String name){
for(int i=0;i<name.length();i++){
System.out.print(name.charAt(i));
}
System.out.println();
}
}
一段时间后,两个线程出现了相互干扰
那么如何才能做到线程安全呢?Duang!!!synchronized出来了,syncronized是java语言的关键字,当他来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码,其实简单一点理解,就是解决了我们前面说的多线程并发的时序性问题,即访问要有一个顺序,讲究先来后到,看谁先拿到这个锁对象,将并行问题转成了串行问题
synchronized的用法
1、方法生命时使用,放在范围操作符(public等)之后,返回类型声明(void)等之前;
如:
public synchionized void synMethod(){
}
2、修饰代码块,对某一代码块使用synchronized(Object),Object是指定的加锁对象;
如:
public void synMethod(){
synchionized(Object){
//一次只能有一个线程进入
}
}
synchionized(Object)中的Object可以是任意对象,可以是当前对象this,也可以是指定对象,但是不建议用当前对象this,可以指定一个很小的对象,如定义1个长度的字节数组。
使用了synchionized的规则如下:
1、当两个并发的线程访问同一个对象Object中的这个synchionized修饰的代码块时,一个时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该段代码块;
2、然而,当一个线程访问对象Object的synchionized(Object)修饰的代码块时,另一个线程仍可以访问其他的非synchionized(Object)修饰的方法
3、当一个线程访问对象Object的synchionized(Object)修饰的代码块时,其他线程对Object中所有的由synchionized(Object)修饰的代码块的访问均被阻塞
4、以上规则对其他对象锁同样适用
我们可以形象的设想一下,synchionized(Object)相当于门的一把锁,Object相当于持有开这把锁的钥匙的人。我们若想进入门内,那么必须先拿到钥匙,当出门后,将门锁上,钥匙还给Object,若我们还在门内,那么Object是没有其他钥匙给其他线程使用的。加入我们多个门(方法)均使用了同样的锁,那么我们不论进入那个门,都需要到Object那拿到钥匙。也就是说多个方法若是synchionized中是同一个的对象时,其中有一个线程已经在访问其中的一个方法,那么其他线程访问另外的其他方法时也会被阻塞。
将线程不安全的代码转化为线程安全
第一种写法:
public synchronized void increamI() throws InterruptedException{
System.out.println(Thread.currentThread().getName()+"--修改前---"+i);
i=i+1;
System.out.println(Thread.currentThread().getName()+"--修改后---"+i);
}
结果:
第二种写法
public void increamI() throws InterruptedException{
synchronized(this){
System.out.println(Thread.currentThread().getName()+"--修改前---"+i);
i=i+1;
System.out.println(Thread.currentThread().getName()+"--修改后---"+i);
}
}
结果
第三种写法
private Byte[] sync = new Byte[1];
public void increamI() throws InterruptedException{
synchronized(sync){
System.out.println(Thread.currentThread().getName()+"--修改前---"+i);
i=i+1;
System.out.println(Thread.currentThread().getName()+"--修改后---"+i);
}
}
结果
现在我们来验证一下上述所说的规则是否正确
1、若synchronized锁得是不同的对象时,会发生什么情况?
我将代码做了小小的改动,如下所示
这样操作得到的结果集:
如何修改可以得到正确的结果集呢?
我们知道类的字节码只有一个,所以用synchronized锁定类的字节码也是可以的。
2、同一个对象中的非synchronized修饰的方法时候正常执行了?synchronized修饰且锁的对象相同时,是否出现了互斥
代码如下,上述打印字符串场景为例
/**
* 线程同步测试
* @author a
*
*/
public class SynchronizedTest {
public void testSync_1(String name){
synchronized (this) {
for(int i=0;i<name.length();i++){
System.out.print(name.charAt(i));
}
System.out.println();
}
}
public void testSync_2(String name){
for(int i=0;i<name.length();i++){
System.out.print(name.charAt(i));
}
System.out.println();
}
public void testSync_3(String name){
synchronized (this) {
for(int i=0;i<name.length();i++){
System.out.print(name.charAt(i));
}
System.out.println();
}
}
}
执行main方法:
public class SyncRun {
public static void main(String[] args) {
final SynchronizedTest st = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
// TODO Auto-generated method stub
st.testSync_1("xiancheng1-------------");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
// TODO Auto-generated method stub
st.testSync_3("HHHHHHHHHHHHHHHH&&&&&&&&&&&&&&&");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
// TODO Auto-generated method stub
st.testSync_2("thread2**************");
}
}
}).start();
}
}
我们观察结果:
要么就是thread2**************字符串与HHHHHHHHHHHHHHHH&&&&&&&&&&&&&&&混在了一起,要么就是thread2**************与xiancheng1————-混在了一起,而HHHHHHHHHHHHHHHH&&&&&&&&&&&&&&&与xiancheng1————-没有任何混淆的时候。
结果图:
由此可以说明:
加了synchronized关键字的方法testSync_1和方法testSync_3,确实是互斥,而他们与方法testSync_2都没有互斥
并发效率问题:
为了适用于高并发对性能及响应速度的要求,synchronized不同的写法程序响应的快慢和对CPU等资源高并发的利用程度又不一样,性能和执行效率的优劣程度有差到优有如下安排
1、同步方法体
public synchronized void synMethod(){
}
小与
public void synMethod(){
synchronized(this){
}
}
小于
public byte[] lock = new byte[1];
public void synMethod(){
synchronized(lock){
}
}
因为锁的对象不一样,锁是对象,加锁和释放锁都需要此对象的资源,那么肯定对象越小越好,所以造一个一字节的byte对象最小