Java 多线程和线程同步

2021-05-07  本文已影响0人  一心729

一. 多线程

1. 分类

A. Thread

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

B. Runnable

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

C. ThreadFactory

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

D. Executor

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

E. Callable

带返回值的异步任务。

2. 使用

二、线程同步

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

线程安全
假如我们自己写了一段程序,运行在主内存中,我们的程序中有一个变量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();
    }
3. ReentrantLock 可重入锁

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

  private final ReentrantLock lock = new ReentrantLock();

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

它和synchronized的区别在哪?

下面在看下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以及锁机制不再更深一步的了解,先点到为止,再加上个人能力有限,如果后面有更深入的理解,再加入进来。

相关问题

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

上一篇 下一篇

猜你喜欢

热点阅读