多线程总结

最通俗易懂的讲解 ThreadLocal 用途:https://www.zhihu.com/question/341005993
Spring中的bean是线程安全的吗:https://cnblogs.com/myseries/p/11729800.html

1. 程序,进程,线程

程序:是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
进程:执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位。
线程:是CPU调度和执行的的单位。通常在一个进程中可以包含若干个线程。



2. 线程的创建

2.1 继承Thread方式

  • 自定义线程类继承Thread
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()启动线程
//模拟火车站售票窗口,开启三个窗口售票,总票数为100张
//存在线程的安全问题(之后例子用线程同步解决)
class Window extends Thread {
    //静态变量,保证票数统一
    static int ticket = 100;

    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "售票,票号为:"
                        + ticket--);
            } else {
                break;
            }
        }
    }
}

public class TestWindow {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        
        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        
        w1.start();
        w2.start();
        w3.start();  
    }
}

2.2 实现Runnable

  • 定义实现类实现Runnable接口
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()启动线程【利用Thread的构造函数,传Runnable实现类对象】
//使用实现Runnable接口的方式,售票
/*
 * 此程序存在线程的安全问题:打印车票时,会出现重票、错票
 */

class Window1 implements Runnable {
    int ticket = 100;

    public void run() {
        while (true) {
            if (ticket > 0) {
                //线程睡眠10秒,暴露重票、错票问题
                try {
                    Thread.currentThread().sleep(10);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售票,票号为:"
                        + ticket--);
            } else {
                break;
            }
        }
    }
}

public class TestWindow1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

2.3 实现Callable【要依赖FutureTaskFutureTask是Future接口的实现类,也可以用作闭锁。】

  • 定义实现类实现Callable接口
  • 重写call()方法,编写线程执行体,需要抛出异常,该方法有返回值
  • 创建线程对象,调用start()启动线程【利用FutureTask的构造函数,传Callable实现类对象,再利用Thread的构造函数,传FutureTask对象】

2.4 继承Thread与实现Runnable的区别

  • 启动方式不同,分别是:子类对象.start 和 传入目标对象+Thread对象.start
  • 继承Thread后,不能再继承其他类,而实现Runnable方式可以。



3. 线程的状态





4. 线程常用方法

  • start():启动线程并执行相应的run()方法
  • run():子线程要执行的代码放入run()方法中
  • currentThread():静态的,调取当前的线程
  • getName():获取此线程的名字
  • setName():设置此线程的名字
  • yield():调用此方法的线程释放当前CPU的执行权,把执行机会让其他优先级相同或更高的线程。
  • join():在A线程中调用B线程的join()方法,表示:当执行到此方法,A线程停止执行,让B线程加入直到其执行完毕,当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止
  • isAlive():判断当前线程是否还存活
  • sleep(long l):显式的让当前线程睡眠l毫秒
  • getPriority() :返回线程优先值
  • setPriority(int newPriority) :改变线程的优先级

4.1 停止线程

不推荐使用JDK提供的stop(),destroy()来停止线程。
推荐让线程自己停下来。
建议使用一个标识符进行终止变量,当flag = false时,终止线程运行。

4.2 线程休眠

方法sleep()用于让当前线程休眠(阻塞),该方法可以传指定时间(毫秒数)。
使用该方法会抛异常InterruptedException。
每一个对象都有一个锁,sleep不会释放锁。

4.3 线程等待

方法wait()用于让当前线程进入等待(阻塞),该方法属于Object的方法。该方法可以传指定时间(毫秒数),当超过指定时间,当前线程就被自动唤醒。
该方法会让当前线程释放锁,直到其他线程调用此对象的notify()或notifyAll()方法。

4.3.1 notify()和notifyAll()

notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

////////////////////  证明wait使当前线程等待  ///////////////////
class ThreadA extends Thread{
    public ThreadA(String name) {
        super(name);
    }
    public void run() {
        synchronized (this) {
            try {                       
                Thread.sleep(1000); //  使当前线阻塞 1 s,确保主程序的 t1.wait(); 执行之后再执行 notify()
            } catch (Exception e) {
                e.printStackTrace();
            }           
            System.out.println(Thread.currentThread().getName()+" call notify()");
            // 唤醒当前的wait线程
            this.notify();
        }
    }
}
public class WaitTest {
    public static void main(String[] args) {
        ThreadA t1 = new ThreadA("t1");
        synchronized(t1) {
            try {
                // 启动“线程t1”
                System.out.println(Thread.currentThread().getName()+" start t1");
                t1.start();
                // 主线程等待t1通过notify()唤醒。
                System.out.println(Thread.currentThread().getName()+" wait()");
                t1.wait();  //  不是使t1线程等待,而是当前执行wait的线程等待
                System.out.println(Thread.currentThread().getName()+" continue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


////////////////////  wait(long timeout)超时被唤醒  ///////////////////
class ThreadA extends Thread{
    public ThreadA(String name) {
        super(name);
    }
    public void run() {     
        System.out.println(Thread.currentThread().getName() + " run ");
        // 死循环,不断运行。
        while(true){;}  //  这个线程与主线程无关,无 synchronized 
    }
}
public class WaitTimeoutTest {
    public static void main(String[] args) {
        ThreadA t1 = new ThreadA("t1");
        synchronized(t1) {
            try {
                // 启动“线程t1”
                System.out.println(Thread.currentThread().getName() + " start t1");
                t1.start();
                // 主线程等待t1通过notify()唤醒 或 notifyAll()唤醒,或超过3000ms延时;然后才被唤醒。
                System.out.println(Thread.currentThread().getName() + " call wait ");
                t1.wait(3000);
                System.out.println(Thread.currentThread().getName() + " continue");
                t1.stop();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4.4 线程优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
优先级的设定建议在start()调度前。
线程默认优先级是5,Thread类中有三个常量,定义线程优先级范围:
static int MAX_PRIORITY 线程可以具有的最高优先级。
static int MIN_PRIORITY 线程可以具有的最低优先级。
static int NORM_PRIORITY 分配给线程的默认优先级。
线程的优先级用数字表示,范围从1~10。
线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。
使用以下方式改变或获取优先级:

  getPriority()
  setPriority(int xxx)


//创建多线程的方式一:继承于Thread类

class PrintNum extends Thread{
   public void run(){
      //子线程执行的代码
      for(int i = 1;i <= 100;i++){
       System.out.println(Thread.currentThread().getName() + ":" + i);
      }
   }
   public PrintNum(String name){
      super(name);
   }
}


public class TestThread {
   public static void main(String[] args) {
      PrintNum p1 = new PrintNum("线程1");
      PrintNum p2 = new PrintNum("线程2");
       //优先级高的获取CPU执行几率高
      p1.setPriority(Thread.MAX_PRIORITY);//10
      p2.setPriority(Thread.MIN_PRIORITY);//1
      p1.start();
      p2.start();
   }
}

4.4 线程通信

Java提供了几个方法解决线程之间的通信问题。




5. 线程安全

5.1 什么是并发

并发:同一个对象被多个线程同时操作。

5.2 什么是线程安全

线程安全:多个线程同时执行时,保证线程的正常执行,且保证有限资源不被出现“污染”等意外情况。
实现线程安全的本质:线程安全其实就是一种等待机制, 多个需要同时访问此对象的线程进入这个对象的等待池形成队列, 等待前面线程使用完毕, 下一个线程再使用。

5.3 什么时候要考虑线程安全的问题

java允许多线程并发控制,当多线程同时操作一个可共享的资源变量时,可能会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程调用,从而保证了该变量的唯一性和准确性。

5.4 实现线程安全的方式

  • 同步方法
同步方法:使用synchronized修饰的方法,就叫同步方法。用于保证线程A在执行该方法时,其他线程只能在外等着。 synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
同步方法默认使用 " this " 或 " 当前类Class对象 " 作为锁。

public synchronized void method(){

}


class Window4 implements Runnable {
    int ticket = 100;// 共享数据
    public void run() {
        while (true) {
            show();
        }
    }
    public synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为:"
                    + ticket--);
        }
    }
}
public class TestWindow4 {
    public static void main(String[] args) {
        Window4 w = new Window4();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  • 同步代码块
同步代码块:用synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
object[锁对象]可以是任何对象 , 但是推荐使用共享资源作为同步监视器。
同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

synchronized(object){

}


//模拟火车站售票窗口,开启三个窗口售票,总票数为100张
class Window2 implements Runnable {
    int ticket = 100;// 共享数据
    public void run() {
      while (true) {
       synchronized (this) {//this表示当前对象,本题中即为w,是否锁的标识
        if (ticket > 0) {
          try {
           Thread.currentThread().sleep(10);
         } catch (InterruptedException e) {
          e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName()
             + "售票,票号为:" + ticket--);
        }
      }
     }
  }
}

public class TestWindow2 {
    public static void main(String[] args) {
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}


代码示例:

package com.xhj.thread;

    /**
     * 线程同步的运用
     * 
     * @author XIEHEJUN
     * 
     */
    public class SynchronizedThread {

        class Bank {

            private int account = 100;

            public int getAccount() {
                return account;
            }

            /**
             * 用同步方法实现
             * 
             * @param money
             */
            public synchronized void save(int money) {
                account += money;
            }

            /**
             * 用同步代码块实现
             * 
             * @param money
             */
            public void save1(int money) {
                synchronized (this) {
                    account += money;
                }
            }
        }

        class NewThread implements Runnable {
            private Bank bank;

            public NewThread(Bank bank) {
                this.bank = bank;
            }

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // bank.save1(10);
                    bank.save(10);
                    System.out.println(i + "账户余额为:" + bank.getAccount());
                }
            }

        }

        /**
         * 建立线程,调用内部类
         */
        public void useThread() {
            Bank bank = new Bank();
            NewThread new_thread = new NewThread(bank);
            System.out.println("线程1");
            Thread thread1 = new Thread(new_thread);
            thread1.start();
            System.out.println("线程2");
            Thread thread2 = new Thread(new_thread);
            thread2.start();
        }

        public static void main(String[] args) {
            SynchronizedThread st = new SynchronizedThread();
            st.useThread();
        }

    }
  • Lock锁
从JDK1.5开始提供。Lock(锁)是一个接口,常用实现类ReentrantLock。

class A{
    private final ReentrantLock lock = new ReenTrantLock(); 

    public void m(){
       lock.lock(); 
       try{ 
          //需保证线程安全的代码; 
       } finally{ 
         lock.unlock(); 
         //如果同步代码有异常,要将unlock()写入finally语句块 
      } 
  } 
}

5.5 什么是死锁

死锁问题的发生:多个线程各自占有一些共享资源, 并且互相等待其他线程占有的资源才能运行 而导致两个或者多个线程都在等待对方释放资源, 都停止执行的情形。 某一个同步块 同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题。



6. ThreadLocal

介绍:他是JDK提供的,用于提供线程本地变量,如果ThreadLocal维护变量时,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。
ThreadLocal是除了加锁这种同步方式【同步方法,代码块,Lock锁】之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

6.1 ThreadLocal的简单使用

常用方法:

        void set(T value)         设置当前线程的线程局部变量的值。
        T get()                   该方法返回当前线程所对应的线程局部变量。
        void remove()             将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
        T initialValue()          返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

        //  ThreadLocal是如何做到为每一个线程维护变量的副本的呢  //////
        //  在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。  //


// 关于set方法的源码 //
//set 方法
public void set(T value) {
      //获取当前线程
      Thread t = Thread.currentThread();
      //实际存储的数据结构类型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,没有则创建map并set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }
  
//getMap方法
ThreadLocalMap getMap(Thread t) {
      //thred中维护了一个ThreadLocalMap
      return t.threadLocals;
 }
 
//createMap
void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

////////// 从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。 ////////// 


// 关于get方法的源码//
//get方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
    
//ThreadLocalMap中getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
       int i = key.threadLocalHashCode & (table.length - 1);
       Entry e = table[i];
       if (e != null && e.get() == key)
            return e;
       else
            return getEntryAfterMiss(key, i, e);
}   

例子:

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("执行remove方法后 : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程2中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("执行remove方法后 : " + localVar.get());
            }
        });


    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }

        t1.start();
        t2.start();
    }
}

6.2 ThreadLocal和Synchronized区别

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的是:

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
  • ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
  • ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的。



7. Spring中的bean是线程安全的吗

结论:不安全。
Spring的Bean作用域(scpoe)类型:

    singleton:单例,默认作用域。                                  
    prototype:原型,每次创建一个新对象。
    request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。
    session:会话,同一个会话共享一个实例,不同会话使用不用的实例。
    global-session:全局会话,所有会话共享一个实例。

    @Scope("prototype")//多实例,IOC容器启动创建的时候,并不会创建对象放在容器在容器当中,当你需要的时候,需要从容器当中取该对象的时候,就会创建。
    @Scope("singleton")//单实例 IOC容器启动的时候就会调用方法创建对象,以后每次获取都是从容器当中拿同一个对象(map当中)。
    @Scope("request")//同一个请求创建一个实例
    @Scope("session")//同一个session创建一个实例

7.1 单例Bean

每个线程都共享一个单例实例Bean,所以不安全。是Spring的默认作用域。
如果单例Bean,是一个无状态Bean【也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。
比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
controller、service和dao层本身并不是线程安全的,只是如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。

7.2 原型Bean

每次都创建一个新对象,线程之间不存在Bean的共享,自然是不会有线程安全的问题。

7.3 @Scope注解

他是用于改变Bean的作用域的。

不加@Scope

@RestController
public class TestController {

    private int var = 0;
    
    @GetMapping(value = "/test_var")
    public String test() {
        System.out.println("普通变量var:" + (++var));
        return "普通变量var:" + var ;
    }
}

输出结果:
    普通变量var:1
    普通变量var:2
    普通变量var:3

加@Scope(value = "prototype")

// 加上@Scope注解,他有多个取值:单例-singleton 多实例-prototype
@Scope(value = "prototype") 
@RestController
public class TestController {

    private int var = 0;
    
    @GetMapping(value = "/test_var")
    public String test() {
        System.out.println("普通变量var:" + (++var));
        return "普通变量var:" + var ;
    }
}

这样一来,每个请求都单独创建一个Controller容器,所以各个请求之间是线程安全的,三次请求结果:
    普通变量var:1
    普通变量var:1
    普通变量var:1

7.4 加@Scope(value = "prototype")注解是不是一定就线程安全

@RestController
@Scope(value = "prototype") // 加上@Scope注解,他有2个取值:单例-singleton 多实例-prototype
public class TestController {
    private int var = 0;
    private static int staticVar = 0;

    @GetMapping(value = "/test_var")
    public String test() {
        System.out.println("普通变量var:" + (++var)+ "---静态变量staticVar:" + (++staticVar));
        return "普通变量var:" + var + "静态变量staticVar:" + staticVar;
    }
}

输出结果:
普通变量var:1---静态变量staticVar:1
普通变量var:1---静态变量staticVar:2
普通变量var:1---静态变量staticVar:3

★★  虽然每次都是单独创建一个Controller,但是扛不住他变量本身是static的呀,所以说呢,即便是加上@Scope注解也不一定能保证Controller 100%的线程安全。  ★★  

7.5 总结

  • 在@Controller/@Service等容器中,默认情况下,scope值是单例-singleton的,也是线程不安全的。
  • 尽量不要在@Controller/@Service等容器中定义静态变量,不论是单例(singleton)还是多实例(prototype)他都是线程不安全的。
  • 默认注入的Bean对象,在不设置scope的时候他也是线程不安全的。
  • 一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的
原文地址:https://www.cnblogs.com/itlihao/p/14504971.html