读懂Java多线程与并发-基础篇

1.几个重要概念

同步与异步
同步调用会等待方法的返回,异步调用会瞬间返回,但是异步调用瞬间返回并不代表你的任务就完成了,它会在后台起个线程继续进行任务。
阻塞和非阻塞
阻塞和非阻塞通常形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。非阻塞允许多个线程同时进入临界区。
并发和并行
并行则是两个任务同时进行,而并发呢,则是一会做一个任务一会又切换到另一个任务。所以单个cpu是不能做到并行的,只能是并发。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用,但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
死锁
是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法进行下去。比如线程1占了B资源,请求A资源,线程2占了A资源,请求B资源。
活锁
线程1可以使用资源,但它让其他线程先使用资源;线程2也可以使用资源,但它也让其他线程先使用资源,于是两者一直谦让,都无法使用资源。

2.使用线程

线程状态转换

有三种方法可以创建线程

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。
public class TestRunnable implements Runnable {
    public void run() {
        
    }
}
public class TestCallable implements Callable {
    public String call() {
        return "HI";
    }
}
public class TestThread extends Thread {
    public void run() {
        
    }
}

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public static void main(String[] args) {
    TestRunnable test1 = new TestRunnable ();
    Thread t1= new Thread(test1);
    t1.start();
    
    TestCallable test2 = new TestCallable();
    FutureTask ft = new FutureTask<>(test2);
    Thread t2= new Thread(ft);
    t2.start();
    System.out.println(ft.get());

    TestThread t3 = new TestThread();
    t3.start();
}

线程中断
当一个线程正在运行时,另一个线程调用对应的 Thread 对象的 interrupt()方法来中断它,只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。但实际上线程并没有被中断,还会继续往下执行。

线程中断的3个方法

  • interrupt() 中断线程
  • isInterrupted() 判断是否被中断
  • interrupted() 判断是否被中断,并清除当前中断状态
public class TestInterrupt  implements Runnable{  
    public void run(){  
        try{  
            System.out.println("in TestInterrupt() - start");  
            Thread.sleep(20000);  
            System.out.println("in TestInterrupt() - woke up");  
        }catch(InterruptedException e){  
            System.out.println("in TestInterrupt() - interrupted");  
        }  
        System.out.println("in TestInterrupt() - leaving");  
    }  

    public static void main(String[] args) {  
        TestInterrupt  test= new TestInterrupt();  
        Thread t = new Thread(test);  
        t.start();  
        //主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间  
        try {  
            Thread.sleep(2000);   
        }catch(InterruptedException e){  
            e.printStackTrace();  
        }  
        System.out.println("in main() - interrupting other thread");  
        //中断线程t  
        t.interrupt();  
        System.out.println("in main() - leaving");  
    }  
} 

最后会输出 in TestInterrupt () - leaving。

3.线程间协作

线程等待与唤醒
调用 wait() 使得线程被挂起,当其他线程调用 notify() 或者 notifyAll() 来唤醒挂起的线程。它们都属于Object,而不属于 Thread。

public class TestWait {

    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }
    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    TestWait test = new TestWait ();
     new Thread(()->test.after()).start();
     new Thread(()->test.before()).start();
}

wait() 和 sleep() 的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。

java.util.concurrent 类库中提供了 Condition 类,可调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。用法跟上面的类似,只是把同步块synchronized换成了同步锁ReentrantLock。
两者的区别:

  • synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  • synchronized 是非公平的,ReentrantLock 默认情况下也是非公平的,但是可以实现公平锁。
  • ReentrantLock 等待可中断,而 synchronized 不行。
  • ReentrantLock 可以同时绑定多个条件Condition。
  • ReentrantLock 可重入,一个线程可以反复得到相同的一把锁。
public class TestAwait{

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

public static class TestJoin {
    int num=0;  
    public void  printNum(){
        for (int i = 0; i < 100; i++){
            num=i;
        }
    }
    
    public static void main(String[] args) {
        TestJoin test = new TestJoin ();
        Thread t=new Thread(()->test.printNum()).start();
                 t.join();
                 System.out.println(test.num);
    }
}

4.内存模型

主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量主要是指共享变量。Java 内存模型规定所有的变量都存储在主内存中,而每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成。


Java 内存模型中定义了以下 8 种操作来完成主内存与工作内存之间交互的实现细节。

  • luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的 load 动作使用。
  • load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的 write 操作使用。
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量值放入主内存的变量中。

三大特性

  • 原子性:指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰。
  • 有序性:在并发时,程序的执行可能就会出现乱序。因为指令有可能被重排。
  • 内存可见性:指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

5.volatile和ThreadLocal

volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
volatile 是一种稍弱的同步机制,在访问 volatile 变量时不会执行加锁操作,也就不会执行线程阻塞,因此 volatilei 变量是一种比 synchronized 关键字更轻量级的同步机制。
volatile 虽然保证了内存可见性和有序性(添加内存屏障的方式来禁止指令重排),但不能保证原子性,所以是线程不安全的。

ThreadLocal是解决线程安全问题一个很好的思路,ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。

public class TestThreadLocal{

//通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal seqNum = new ThreadLocal(){

      public Integer initialValue(){
            return 0;
    }

};

public int getNextNum(){

    seqNum.set(seqNum.get()+1);

     return seqNum.get();

}

public static void main(String[] args) {
    
    TestThreadLocal test = new TestThreadLocal();
    new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
    new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
     new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
  }
}

ThreadLocal和线程同步机制比较

  • 同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的。
  • ThreadLocal会为每一个线程拷贝一份独立的变量副本,从而隔离了多个线程对数据的访问冲突。
    当然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。

参考资料
高并发Java
线程通信

你可能感兴趣的:(读懂Java多线程与并发-基础篇)