• java并发编程实践线程安全性


    线程是CPU资源调度的基本单位,如果一个程序中只有一个线程,则最多只能在一个处理器上运行,如果电脑/服务器是双处理器系统,则单线程的程序只能使用一半的CPU资源,所以,多线程是提高处理器资源利用率的重要方法。比如web系统中的servlet容器,它处理请求时会针对每一个请求创建一个线程调用servlet的service方法(https://m.runoob.com/servlet/servlet-life-cycle.html),servlet的实例在容器中只会创建一个,所以就涉及到了并发操作问题(多个线程访问处理同一个资源),下图摘自菜鸟教程中给出的一个请求示例:

     从上图中可以看出,如果service()方法对某一个可变的公共变量做了操作,线程a,b,c如果未作合适的同步控制的话,程序必然会出现错误,修复这个问题有三种方式:

    a.不在线程之间共享该状态变量,也就是service()方法不会调用公共的变量/资源。

    b.将状态变量设置为不可修改的变量,也就是service()方法调用的公共变量的值是一个恒定的值,各个线程也不能对该变量进行修改。

    c.在访问公共的状态变量时进行同步控制,也就是线程b/c要等线程a对公共变量的操作执行完毕之后才能获取访问权。

    一个无状态的对象一定是线程安全的,如下示例:

    package main;
    
    import javax.servlet.*;
    import java.io.IOException;
    
    public class StatelessFactorizer implements Servlet {
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            int id=(int)req.getAttribute("id");
            id++;
            req.setAttribute("id",id);
        }    
        @Override
        public void init(ServletConfig config) throws ServletException {
            
        }
    
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
    
        @Override
        public String getServletInfo() {
            return null;
        }
    
        @Override
        public void destroy() {
    
        }
    }
    View Code

    上面的代码中,id虽然做了++操作,但是它是属于方法的内部变量,也是线程私有的,这个值是从前端带过来的,所以这是一中“无状态的”操作方法。

    package main;
    
    import javax.servlet.*;
    import java.io.IOException;
    
    public class UnsafeCounting implements Servlet {
        private long count=0;
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            count++;
            req.setAttribute("count",count);
        }
        @Override
        public void init(ServletConfig config) throws ServletException {
    
        }
    
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
    
        @Override
        public String getServletInfo() {
            return null;
        }
    
        @Override
        public void destroy() {
    
        }
    }
    View Code

    再如上面的示例,UnsafeCounting实例的service方法由多个线程调用时,每个线程都会操作count这个公共资源,但是count++这种操作方法并不是原子性的,所以这种操作不是线程安全的。这里定义一个术语:

    竞态条件(Race Condition):并发编程时,由于不恰当的执行时序而出现不正确的结果的情况。

    对于有竞态条件出现的情况下,需要使用“加锁机制”来保证操作步骤的原子性,也就是多个线程对操作步骤的顺序执行,java提供了一种内置的锁机制来实现原子操作:同步代码块(synchronized block),同步代码块由两部分组成:

    a.加锁的对象,是一个引用,也就是说对哪个对象进行加锁,确定了加锁的对象,所有的线程要先拿到这个对象的锁才能进行下一步的操作

    b.由该锁保护的代码块,也就是哪些操作步骤必须是原子操作,就将这些需要作为原子操作的步骤放到锁所保护的代码块种即可

    如下示例:

    synchronized(obj){//todo 需要是原子操作的步骤}

    **锁的重入**

    重入是一个重要特征,就是一个线程获取到了某个锁的执行权,它再次获取该锁时,需要保证能够获取成功,

    package main;
    
    public class Widget {
    
        public synchronized void doSomething(){
            System.out.println("Widget doSomething "+this);
        }
    
        public static void main(String[] args) {
            LoggingWidget loggingWidget=new LoggingWidget();
            loggingWidget.doSomething();
        }
    }
    
    class LoggingWidget extends Widget{
        @Override
        public synchronized void doSomething() {
            System.out.println("LoggingWidget doSomething "+this);
            super.doSomething();
        }
    }
    ---------输出--------
    LoggingWidget doSomething main.LoggingWidget@74a14482
    Widget doSomething main.LoggingWidget@74a14482
    View Code

    上面的代码中,synchronized方法修饰的是非静态方法,它的锁对象是this,也就是方法调用所在的对象,如果synchronized放到静态方法上,则其加锁的对象是Class对象;从上面的打印中,super中输出的this和子类中输出的this是一样的,如果synchronized锁不能重入的话,子类在调用doSomething的时候已经获取到了“main.LoggingWidget@74a14482”的锁,就不能调用super.doSomething()了,显然是不合理的;锁的重入则很完美地避免了该问题的产生。

    是不是为了简单起见,直接将synchronized加到方法上得了?当然不是,因为还要考虑性能问题,如下实例:

    class DownLoadFile extends HttpServlet{
        @Override
        protected synchronized void service(HttpServletRequest req,
                               HttpServletResponse resp) throws ServletException, IOException {
            //todo 下载一个大文件
            File file=new File("c:/xxxx/uuu");
                   
            //todo 对某个公共资源进行操作
        }
    }
    View Code

    上面的代码中,下载大文件的操作比较费时,如果对方法进行加锁,则性能很差,这时候可以不对下载大文件进行加锁,如下:

    class DownLoadFile extends HttpServlet{
        @Override
        protected  void service(HttpServletRequest req,
                               HttpServletResponse resp) throws ServletException, IOException {
            //todo 下载一个大文件
            File file=new File("c:/xxxx/uuu");
            synchronized(this){
                //todo 对某个公共资源进行操作
            }        
        }
    }
    View Code

    上面的代码中,性能将会提升很多,因为每个请求可以发送请求后立马进行文件下载操作,文件下载完毕后再尝试获取锁对公共资源进行操作。

    以上是Java线程安全性的总结,在保证安全的同时也需要考虑性能问题,因此加锁时需要做到:

    a.识别哪些操作步骤需要是原子性操作,以保证程序的安全性

    b.判断哪些操作比较费时,这些比较费时的操作一定不要持有锁,因为会严重影响程序性能,记住,只对需要加锁的步骤进行加锁就可以了

  • 相关阅读:
    laravel数据库配置
    mysql中utf8和utf8mb4区别
    laravel中artisan的用法
    laravel项目composer安装
    Linux 文件描述符
    Centos 7/8 SELinux
    Centos 7/8 高级磁盘管理技术
    Centos 7/8 搭建NFS Server
    Centos 7/8 日志管理
    Centos 7/8 Chronyd时间同步
  • 原文地址:https://www.cnblogs.com/codeMedita/p/16099308.html
Copyright © 2020-2023  润新知