Java 多线程和线程同步

一. 多线程

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锁对象,二者互相等待对方释放锁对象,就死等,这一等就是一辈子。

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

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

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

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容