美文网首页
Java 多线程和线程同步

Java 多线程和线程同步

作者: 一心729 | 来源:发表于2021-05-07 00:28 被阅读0次

    一. 多线程

    1. 分类

    A. Thread

    最常用的开启新线程的方式,最终的调用是由Java虚拟机根据不同平台来执行不同的调用,因为start0最终是一个native方法。

    B. Runnable

    通过源码可以得知,Runnable的run方法最终是被Thread中的run方法执行的。它和Thread的区别在于可以重用,把有可能重用的代码封装到Runnable中。

    C. ThreadFactory

    标准的工厂设计模式,通过工厂设计模式来统一提供 Thread对象,可以对Thread对象做统一的处理工作。

    D. Executor

    线程池。这也是我们在实际当中使用最多的多线程的工具。通过线程池我们可以获取很多内置的应用不同场景下的多线程。

    E. Callable

    带返回值的异步任务。

    2. 使用

    • Thread

         new Thread() {
            @Override
             public void run() {
                 //do something
            }
         }.start();
      
    • Runnable

        Runnable runnable = new Runnable() {
          @Override
          public void run() {
              //do something
          }
        };
        Thread thread = new Thread(runnable);
        thread.start();
      
    • ThreadFactory

      ThreadFactory threadFactory = new ThreadFactory() {
          private AtomicInteger threadCount = new AtomicInteger(0);
          @Override
          public Thread newThread(Runnable runnable) {
              return new Thread(runnable, "Thread-number " + threadCount.incrementAndGet());//++threadCount
          }
      };
      Runnable runnable = new Runnable() {
          @Override
          public void run() {
              //do something
          }
      };
      Thread thread = threadFactory.newThread(runnable);
      thread.start();
      Thread thread1 = threadFactory.newThread(runnable);
      thread1.start();
      
    • Executor

      Runnable runnable = new Runnable() {
          @Override
          public void run() {
              //do something
          }
      };
      Executor executor = Executors.newCachedThreadPool();
      executor.execute(runnable);
      

      内置的常用线程池说明

      1. Executors.newFixedThreadPool(threadCount)
        获取固定线程数量的线程池。用于处理临时性爆发式任务,比如图片的处理等。
      2. Executors.newSingleThreadExecutor()
        获取单个线程的线程池,这个用途比较少,比如当取消所有任务的时候可以用这个。
      3. Executors.newCachedThreadPool()
        带缓存的线程池工具,默认固定线程数量为0,没有线程数量的上限,无活跃60s回收。
      4. Executors.newXXXScheduledExecutor()
        具有延迟功能的线程池工具。
      5. ThreadPoolExecutor构造函数参数说明:
        corePoolSize:线程池默认固定的线程数量,当线程池空闲时维持的最小线程数。
        maximumPoolSize:线程池允许创建最多的线程数量。
        keepAliveTime:当创建的线程数量超过corePoolSize的数量后,所创建的线程在指定时间内如果无活动,则被回收。
        unit:参数keepAliveTime的时间单位。
        workQueue:用于保存execute方法提交的Runnable的队列。
        threadFactory:当Executor需要创建新的线程时,由该工厂提供新线程的创建。
    • Callable

      Callable<String> callable = new Callable<String>() {
          @Override
          public String call() throws Exception {
              Thread.sleep(3000);
              return "Done!!!";
          }
      };
      ExecutorService executorService = Executors.newCachedThreadPool();
      Future<String> future = executorService.submit(callable);
      try {
          String result = future.get();
      } catch (ExecutionException e) {
          e.printStackTrace();
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      

    二、线程同步

    当多个线程对同一资源进行操作,如对一个变量进行赋值操作,此时就牵涉到了线程安全的问题,就是所谓的线程同步。当我们在编写程序的时候,发现某个变量有可能会被多个线程同时访问和操作的时候,就要考虑到线程同步的问题,所谓的并发编程。接下来说下为什么会出现这种线程安全的问题,如下图:

    线程安全
    假如我们自己写了一段程序,运行在主内存中,我们的程序中有一个变量x初始值为0.当线程A想要访问x变量时,会先将变量x拷贝到自己的线程所属的内存中。当线程A修改了x变量的值为1,在合适的时机将修改后的变量值同步到主内存中。线程B再去取主内存中x值的时候已经是修改后的值。
    上述过程是没有问题的,这也是我们所期望的。
    为什么会这么设计呢?为什么不直接从主内存中取呢?因为当其他线程和主内存频繁的IO操作时,效率是非常低的。而将主内存的值拷贝到线程自己的高速缓存中,再同步到主内存中,这种设计的效率是比直接操作主内存高出几十倍的。
    那我们这么解决这种情况下的问题呢?
    答案是:线程同步

    在Java中怎样实现线程同步呢?

    1. volatile关键字
    private volatile boolean isOpen = true;
    

    将修饰的变量的线程同步性强制打开。线程会以最积极的同步方式进行线程间的同步。使用变量前会先从主内存中同步,修改之后会立即同步到主内存。虽然保证了线程的安全性,但是效率却很低,所以只有当我们需要的时候才去打开。但是volatile关键字只对原子操作有效,非原子操作无效。例如:

    private volatile int x = 0;
    
    private void count(){
        x++;
    }
    

    在这个例子中volatile关键字是无效的,原因是因为x++在实际运行中是分两步的:
    1.int temp = x + 1;
    2. x = temp;
    这个操作是非原子操作。

    2. synchronized关键字

    synchronized关键字的出现完美的解决了volatile关键字的局限性。
    作用:
    1. 保证同步性,即是volatile的特性;
    2. 互斥访问,对代码块中的资源进行保护,保证了资源的同步性,即原子操作。
    下面我们画一下synchronized的工作模型。

    synchronized工作模型1
    当线程A访问对象Test中的方法A的时候,由于加了synchronized关键字,线程A会先访问monitor,询问下是否可以访问方法A,如果可以访问,则直接访问方法A,此时monitor状态为不可访问。当线程B进行访问方法A的时候,同样也需要先询问 monitor,此时的monitor的是不可访问的,所以线程B是不可以访问方法A的,只能等线程A访问结束后,monitor监视器的状态为可访问时,线程B才可以访问方法A。
    不同的方法可以加不同的monitor,使用synchronized代码块,传入不同的对象,即可实现不同的monitor,即synchronized(monitor){},如下图:
    多个monitor
    代码实现如下:
        private final Object monitorA = new Object();
        private final Object monitorB = new Object();
    
        private void methodA() {
            synchronized (monitorA){
                //do something
            }
        }
    
        private void methodB() {
            synchronized (monitorB) {
                //do something
            }
        }
    
        private void methodC() {
            synchronized (monitorB){
                //do something
            }
        }
    
        public void test() {
            Thread threadA = new Thread(){
                @Override
                public void run() {
                    methodA();
                }
            };
            threadA.start();
            Thread threadB = new Thread(){
                @Override
                public void run() {
                    methodA();
                    methodB();
                }
            };
            threadB.start();
            Thread threadC = new Thread(){
                @Override
                public void run() {
                    methodC();
                }
            };
            threadC.start();
        }
    
    • 如果synchronized修饰的是方法,则monitor默认传入的是该方法所在的对象,即this,代码如下:
        private synchronized void method() {
            //do something
        }
      
    • 如果synchronized修饰的是静态方法,则monitor需要传入静态对象,如:静态变量,**.class等,**.class一般传入对应类的.class,代码如下:
        class Test{
            private static synchronized void method() {
                //do something
            }
        }
      
      上述代码中传入的monitor,默认为该静态方法所在类文件的.class对象,即Test.class
       class Test{
           private static void method() {
               synchronized(Test.class){
                   //do something
               }
           }
       }
      
      上述代码可以传入指定静态的monitor对象,这种常用于单例对象的获取。
      这里我们也顺便说下单利对象的写法吧,一般这样写足够安全了,其中如果单利对象初始化对象的时候,需要依赖注入,则要加上volatile关键字,保证初始化对象的同步性,即初始化完成之后,对象才标记为非空,否则可以不加。
       public final class SingleMan {
       private static volatile SingleMan sInstance;
       private SingleMan(String tag) {}
      
       public static SingleMan getInstance(){
           if (sInstance == null) {
               synchronized (SingleMan.class) {
                   if (sInstance == null) {
                       sInstance = new SingleMan("SingleMan");
                   }
               }
           }
           return sInstance;
         }
       }
      
    3. ReentrantLock 可重入锁

    这是一个手动锁,上锁和解锁都需要写代码的人去完成,而且异常情况也需要自己处理。我们通过代码来看下它常规的使用。

      private final ReentrantLock lock = new ReentrantLock();
    
      private void methodA() {
          lock.lock();//上锁
          try {
              //do something
          } finally {
              lock.unlock();//不管是否异常,最终都会释放锁
          }
      }
    

    它和synchronized的区别在哪?

    • synchronized是最为关键字出现的,上锁和解锁以及异常处理,JVM已经帮我们做了,而ReentrantLock不是关键字,上锁和解锁已经异常处理需要我们手动完成。
    • synchronized不能具体区分出读锁和写锁,而 ReentrantLock可以分别加读锁和写锁,所以相对于synchronized而言ReentrantLock锁粒度更细。

    下面在看下ReentrantLock的读锁和写锁的常规使用,举个简单的栗子。

      private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
      private final Lock readLock = lock.readLock();
      private final Lock writeLock = lock.writeLock();
    
      private int x = 0;
    
      private void count() {
          writeLock.lock();
          try {
              x++;
          } finally {
              writeLock.unlock();
          }
      }
    
      private void printNumber() {
          readLock.lock();
          try {
              System.out.println("The number is " + x);
          } finally {
              readLock.unlock();
          }
      }
    

    以上代码,当执行count方法时,其他线程是不可以对x进行写操作,但是可以进行读操作,同样,当执行printNumber方法时,其他线程是不可以对x进行读操作,但是可以进行写操作,因为读写锁是分离的。

    这里对ReentrantLock以及锁机制不再更深一步的了解,先点到为止,再加上个人能力有限,如果后面有更深入的理解,再加入进来。

    相关问题

    • 进程和线程有什么区别?
      线程是运行在进程中的,每个操作系统中有许多个进程,每个进程都是相互独立的。
      举个通俗的例子,这个世界就是一个大的操作系统,每一个家庭代表着一个进程,而每个家庭中的人代表着每一个线程。每个家庭可以进行沟通,而每个进程也遵循一定的协议进行沟通,每个人可以协同完成某一任务,每个线程也可以通过协同合作来完成某一个任务。
      往深的说,线程间共享资源,进程间不共享。而且这俩都不是一个概念,不能进行比较,共同点就是可以同时并行。

    • 死锁
      死锁只出现在锁关系比较复杂的情况下,即锁嵌套,单个锁是不会出现的。如下代码则会出现死锁的情况:

        public void method1(){
            synchronized(monitor1){
                //do something
                synchronized(monitor2){
                    //do something
                }
            }
        }
        
        public void method2(){
            synchronized(monitor2){
                //do something
                synchronized(monitor1){
                    //do something
                }
            }
        }
      

      方法1和方法2互相持有monitor1monitor2的所对象,就会出现,方法1中执行到synchronized(monitor2){}这句代码时,会等待方法2释放monitor2锁对象,同样,方法2执行到synchronized(monitor1){}这句代码时,会等待方法1释放monitor1锁对象,二者互相等待对方释放锁对象,就死等,这一等就是一辈子。

    • 乐观锁和悲观锁
      在后端开发中,经常会遇到这种问题,当从数据库中读出数据后,然后啪啪啪进行一顿业务操作,之后再将数据写回数据库中,这时出现以下情况:

      • 在自己从数据库中读数据之后到写数据之前,数据库中的数据可能已经被其他伙伴修改过了。

      遇到这种情况,做法如下:

      • 假设我们读的时候不上锁,写回数据库的时候再检查数据是否发生了改变,之后根据具体在写回数据库,写的过程是一定加锁的,这个处理方式即为乐观并发控制,所谓乐观锁

      • 假设我们在读数据之前,就先上锁,不允许其他伙伴进行读写操作,直到自己将锁释放后,别的伙伴才可以读写,这个处理方式即为悲观并发控制,所谓悲观锁

      以上两种处理方式皆为处理并发操作的思想

    由于个人能力有限,如有错误之处,还望指出,我会第一时间验证并修改。
    理解事物,看本质。共勉。

    相关文章

      网友评论

          本文标题:Java 多线程和线程同步

          本文链接:https://www.haomeiwen.com/subject/nlnkdltx.html