多线程学习总结(二):同步与并发

#多线程学习总结(二):同步与并发

最近看了《Java编程多线程核心技术》这本书,在此接着上一篇继续总结以下知识点:

  • 线程安全问题的产生
  • synchronized关键字的用法
  • volatile关键字的用法
  • 原子类的用法
    1.线程安全问题的产生

举个简单的例子

public void test(){
        Runnable t= new Runnable(){
            int i=5;
            @Override
            public void run() {
                i--;
                System.out.println(i+"==============="+Thread.currentThread().getName());
            }
        };
        new Thread(t,"t1").start();
        new Thread(t,"t2").start();
        new Thread(t,"t3").start();
        new Thread(t,"t4").start();
        new Thread(t,"t5").start();
    }

运行结果如下
这里写图片描述
原因分析:因为上述五个线程共享了同一个数据i,而根据上篇文章的分析,线程的运行具有随机性,具体来说,在上述例子中,run()方法里有两条语句:先自减一,再println()输出,五个线程可能某一个执行完自减一,操作系统时间片被别的线程抢走,再减一…,等到第一个线程执行输出时,i可能早已被修改了好几次,所以出现了图中的结果,而不是5,4,3,2,1依次输出。这就是线程安全问题,在涉及多线程的程序里,线程安全问题必须小心处理,否则可能会发生难以预料的后果。
**注意:**只有在多个线程共享数据是才会存在线程安全问题,如果没有共享变量,即每个线程里各自有各自的变量的话就不可能存在线程安全问题,各用各的,互不打扰。
线程:

2.synchronized关键字的用法

1.修饰方法,在方法返回值之前加上synchronized方法,就将方法变为了同步方法
2.修饰代码块。

synchronized (object){
            ...//需要同步的代码块
            } 

后面括号内的object就是该代码块的锁对象。
线程在执行被synchronized修饰的方法和代码块时,必须先得到锁,才能执行该方法和代码块,由此产生同步的效果。
注意事项:
1.同步原理:java中每一个类和对象都与一个对象监视器相关联,被 synchronized修饰的方法和代码块就相当于标记了监视区域,当执行到该区域时,jvm就会自动锁上该类或者对象,别的线程必须等到该线程执行完毕之后才能进入该区域。
2.两种方法联系:synchronized修饰方法时,锁对象就是this,也就是该方法的类的实例,这意味着,synchronized修饰方法就相当于synchronized (this)修饰代码块(synchronized修饰静态方法则相当于synchronized (this.class)修饰代码块,这就是类锁,详见第六条),但前者显然没有后者灵活,后者的锁不仅可以是this,也可以是任何对象,且控制粒度更小.
3.锁重入:当一个线程进入被synchronized修饰的方法和代码块时,它可以再次请求并得到该对象的锁,即一个线程在同步方法和块内调用同一个锁对象的其它方法和块,该线程可一直得到该锁。这就是锁重入。
4.同步不具有继承性,在父类的同步方法被子类覆写时,如果子类没有加synchronized方法,则该方法将不具有同步的功能。
5.异常时锁自动释放
6.类锁当synchronized修饰静态方法时,由于静态方法是属于类的,所以该方法是给类加了锁,这就是类锁,与对象锁的使用无太大区别,但是注意,类锁和对象锁是两把锁,类锁对类和它的所有对象生效,看如下例子

//定义的用于测试的类
 class SynchronizedTest {
// A方法为静态方法
    public synchronized static void methodA(){
        System.out.println(Thread.currentThread().getName()+":methodA-------------start");
        try {
        Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":methodA-------------end");
    }
    //B方法为普通方法
    public synchronized void methodB(){
        System.out.println(Thread.currentThread().getName()+":methodB-------------start");
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":methodB-------------end");
    }
}
    
     public void test(){
     //该方法进行测试
        SynchronizedTest synchronizedTest=new SynchronizedTest();
        
        new Thread(()->SynchronizedTest.methodA(),"AA").start();//1
        new Thread(()->synchronizedTest.methodB(),"BB").start();//2
        new Thread(()->synchronizedTest.methodA(),"CC").start();//3
    }
    

执行结果
这里写图片描述
由结果分析(可分别注释掉上面test方法中的三个语句进行多次验证)可知,A线程与C线程互斥,AB,BC都不互斥,证明了AC为同一把锁,B为一把锁。

3.volatile关键字的用法

volatile用于修饰变量,保证变量的可见性,
在某些jvm中,为了提高运行效率,每个线程会单独开辟一个工作内存,这样在线程内部改变了某个变量,其它线程就无法立即得到改变后的值,但volatile修饰该变量,则该变量就不会放入工作内存,强制从主内存中读取和写入变量的值,这样多个线程在共享一个变量时,就一定程度上保证了线程安全,但不是绝对的。
关于volatile的作用,就不上代码了,看如下两幅示意图便知。
这里写图片描述
volatile与synchronized 关键词的比较:
1.volatile是线程安全的轻量级实现,应能高于synchronized,volatile只修饰变量,而synchronized修饰方法和代码块。
2.多线程访问volatile不会阻塞,而synchronized会阻塞。
3.volatile解决了多线程的可见性,但无法保证原子性(关于原子性,请看下面关于原子类的讲解),而synchronized可以保证原子性和可见性。

原子性 一个操作不能被再拆分了;即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。一个很经典的例子就是银行账户转账问题。
可见性 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

四、原子类的用法

原子类能够实现原子操作的类,这些类可以帮助我们在没有锁的情况下保证线程安全。

**无锁:**关于线程安全,人们有乐观派和悲观派两种看法,乐观派认为事情总会往好的方向发展,所以倾向于不加锁。但悲观派会认为发展事态如果不及时控制,以后就无法挽回了,所以倾向于加锁。原子类就是基于乐观策略而产生的工具包,通过原子操作保证在对变量进行改变时的线程安全。

原子类的包:java.util.concurrent.atomic
常用类:
AtomicBoolean//原子布尔类
AtomicInteger//原子整型类
AtomicLong//原子长整型
AtomicReference//原子引用类型
以上原子类在内部分别封装了一个布尔型,整型,长整型,引用类型的变量
两个常用方法如下,getAndSet(newValue)得到原本的值并写入新值,
compareAndSet(expectedValue, newValue)如果目前值等于expectedValue,则把newValue赋给它。
其它如AtomicInteger.getAndAdd(),返回并把当前值加1,相当于i++;
AtomicInteger.addAndGet(),当前值加1并返回,相当于++i;
注意++i和++i都不是线程安全的,而上述两个方法均是原子操作,在该方法的调用上实现了线程安全。
原子类的原子操作都是通过底层的CAS算法完成的

CAS:全称是Compare And Swap 即比较交换,其算法核心思想如下:
执行函数:CAS(V,E,N)
V表示要更新的变量
E表示预期值
N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作

以上所有内容均总结自高洪岩的《Java编程多线程核心技术》一书

我的系列文章

多线程学习总结(一):基础知识
多线程学习总结(二):同步与并发
多线程学习总结(三):线程间通信
多线程学习总结(四):可重入锁和读写锁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值