多线程复习

多线程复习

  • 进程和线程的区别
  • 并行和并发的区别
  • 线程的生命周期
  • 创建线程的三种方式
  • 什么是多线程
  • 线程池
  • 线程中的sleep方法和wait方法
  • 什么是线程安全问题
  • synchronized、Lock、ReentrantLock、ReadWriteLock。
  • volatile关键字

进程和线程的区别

进程: 正在执行的程序,是表示资源分配的基本单位,又是调度运行的基本单位
线程:线程是程序执行的一条路径,是进程中执行运算的最小单位,亦即执行处理机调度的基本单位
并行 就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行

线程的生命周期

新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态

  1. 新建和就绪状态
      当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量

值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该

线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

  1. 运行和阻塞状态
      如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程处于运行状态。
    当发生如下情况下,线程将会进入阻塞状态:
    线程调用sleep方法主动放弃所占用的处理器资源。
    线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
    线程试图获得一个同步监视器,但该同步监视器正被其他线程锁持有。关于同步监视器的知识将在后面有更深入的介绍。
    线程在等待某个通知(notify)。

程序调用了线程的suspend方法将该线程挂起。不过这个方法容易导致死锁,所以程序应该尽量避免使用该方法。
当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会了。被阻塞的线程会在合适时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。
针对上面的几种情况,当发生如下特定的情况将可以解除上面的阻塞,让该线程重新进入就绪状态:

调用sleep方法的线程经过了指定时间。

线程调用的阻塞式IO方法已经返回。

线程成功地获得了试图取得同步监视器。

线程正在等待某个通知时,其他线程发出了一个通知。

处于挂起状态的线程被调用了resume恢复方法。

线程状态转换图:
在这里插入图片描述
[
3. 线程死亡
线程会以以下三种方式之一结束,结束后就处于死亡状态:

run()方法执行完成,线程正常结束。

线程抛出一个未捕获的Exception或Error。

直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

创建线程的三种方式
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程

Thread继承
* 定义类继承Thread
* 重写run方法
* 把新线程要做的事写在run方法中
* 创建线程对象
* 开启新线程, 内部会自动执行run方法

			/**
			 * @param args
			 */
			public static void main(String[] args) {
				MyThread mt = new MyThread();							//4,创建自定义类的对象
				mt.start();												//5,开启线程
				
				for(int i = 0; i < 3000; i++) {
					System.out.println("bb");
				}
			}
		
		}
		class MyThread extends Thread {									//1,定义类继承Thread
			public void run() {											//2,重写run方法
				for(int i = 0; i < 3000; i++) {							//3,将要执行的代码,写在run方法中
					System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
				}
			}
		}

Runnable接口

  • 定义类实现Runnable接口
    • 实现run方法
    • 把新线程要做的事写在run方法中
    • 创建自定义的Runnable的子类对象
    • 创建Thread对象, 传入Runnable
    • 调用start()开启新线程, 内部会自动调用Runnable的run()方法
public class Demo3_Runnable {
				/**
				 * @param args
				 */
				public static void main(String[] args) {
					MyRunnable mr = new MyRunnable();						//4,创建自定义类对象
					//Runnable target = new MyRunnable();
					Thread t = new Thread(mr);								//5,将其当作参数传递给Thread的构造函数
					t.start();												//6,开启线程
					
					for(int i = 0; i < 3000; i++) {
						System.out.println("bb");
					}
				}
			}
			
			class MyRunnable implements Runnable {							//1,自定义类实现Runnable接口
				@Override
				public void run() {											//2,重写run方法
					for(int i = 0; i < 3000; i++) {							//3,将要执行的代码,写在run方法中
						System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
					}
				}
				
			}

多线程(实现Runnable的原理)

  • 查看源码
    • 1,看Thread类的构造函数,传递了Runnable接口的引用
    • 2,通过init()方法找到传递的target给成员变量的target赋值
    • 3,查看run方法,发现run方法中有判断,如果target不为null就会调用 Runnable接口子类对象的run方法

Callable和Future创建线程链接: https://www.jb51.net/article/145487.htm
和Runnable接口不一样,Callable接口提供了一个call方法作为线程执行体,call()方法比run()方法功能要强大。

  • call()方法可以有返回值
  • call()方法可以声明抛出异常
    Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
boolean isDone():若Callable任务完成,返回True
boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
/*
 * 一、创建执行线程的方式三:实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
 *
 * 二、执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。  FutureTask 是  Future 接口的实现类
 */
public class TestCallable {
 
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
 
        //1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
        FutureTask<Integer> result = new FutureTask<>(td);
 
        new Thread(result).start();
 
        //2.接收线程运算后的结果
        try {
            Integer sum = result.get();  //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
            System.out.println(sum);
            System.out.println("------------------------------------");
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
 
}
 
class ThreadDemo implements Callable<Integer> {
 
    @Override
    public Integer call() throws Exception {
        int sum = 0;
 
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
 
        return sum;
    }
 
}

Runnable接口和Callable接口的区别。

Callable接口和Runnable接口相似,区别就是Callable需要实现call方法,而Runnable需要实现run方法;并且,call方法还可以返回任何对象,无论是什么对象,JVM都会当作Object来处理。但是如果使用了泛型,我们就不用每次都对Object进行转换了。
Runnable和Callable都是接口
相同点
都是接口

都可以编写多线程程序

都采用Thread.start()启动线程
不同之处
1.Callable可以返回一个类型V,而Runnable不可以,Runnable没有返回值;Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
2.Callable能够抛出checked exception,而Runnable不可以。
3.Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
4.Callable和Runnable都可以应用于executors。而Thread类只支持Runnable.
5.Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛

注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
上面只是简单的不同,其实这两个接口在用起来差别还是很大的。Callable与executors联合在一起,在任务完成时可立刻获得一个更新了的Future。而Runable却要自己处理

Future接口,一般都是取回Callable执行的状态用的。其中的主要方法:
cancel,取消Callable的执行,当Callable还没有完成时
get,获得Callable的返回值
isCanceled,判断是否取消了
isDone,判断是否完成
用Executor来构建线程池,应该要做的事:
1).调用Executors类中的静态方法newCachedThreadPool(必要时创建新线程,空闲线程会被保留60秒)或newFixedThreadPool(包含固定数量的线程池)等,返回的是一个实现了ExecutorService 接口的ThreadPoolExecutor类或者是一个实现了ScheduledExecutorServiece接口的类对象。
2).调用submit提交Runnable或Callable对象。
3).如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象。
4).当不再提交任何任务时,调用shutdown方法。

多线程 https://www.cnblogs.com/wxd0108/p/5479442.html

  • 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程

Java通过Executors提供四种线程池

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

1.newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3.newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4.newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

单任务线程池,newSingleThreadExecutor:

	package app.executors; 
 
import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; 
 
/** 
 * Java线程:线程池 
 * 
 * @author xiho
 */
public class Test { 
  public static void main(String[] args) { 
  //创建一个单线程线程池
    ExecutorService pool = Executors.newFixedThreadPool(); 
    // 创建线程 
    Thread t1 = new MyThread(); 
    Thread t2 = new MyThread(); 
    Thread t3 = new MyThread(); 
    Thread t4 = new MyThread(); 
    Thread t5 = new MyThread(); 
    // 将线程放入池中进行执行 
    pool.execute(t1); 
    pool.execute(t2); 
    pool.execute(t3); 
    pool.execute(t4); 
    pool.execute(t5); 
    // 关闭线程池 
    pool.shutdown(); 
  } 
} 
 
class MyThread extends Thread { 
  @Override
  public void run() { 
    System.out.println(Thread.currentThread().getName() + "正在执行。。。"); 
  } 
}

pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
可以看出,每次调用execute方法,其实最后都是调用了thread-1的run方法。

固定大小的线程池,newFixedThreadPool:

	package app.executors; 
 
import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; 
 
/** 
 * Java线程:线程池 
 * 
 * @author xiho
 */
public class Test { 
  public static void main(String[] args) { 
    // 创建一个可重用固定线程数的线程池 
    ExecutorService pool = Executors.newFixedThreadPool(2); 
    // 创建线程 
    Thread t1 = new MyThread(); 
    Thread t2 = new MyThread(); 
    Thread t3 = new MyThread(); 
    Thread t4 = new MyThread(); 
    Thread t5 = new MyThread(); 
    // 将线程放入池中进行执行 
    pool.execute(t1); 
    pool.execute(t2); 
    pool.execute(t3); 
    pool.execute(t4); 
    pool.execute(t5); 
    // 关闭线程池 
    pool.shutdown(); 
  } 
} 
 
class MyThread extends Thread { 
  @Override
  public void run() { 
    System.out.println(Thread.currentThread().getName() + "正在执行。。。"); 
  } 
}

pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-4正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-5正在执行。。。
从以上结果可以看出,newFixedThreadPool的参数指定了可以运行的线程的最大数目,超过这个数目的线程加进去以后,不会运行。其次,加入线程池的线程属于托管状态,线程的运行不受加入顺序的影响。

可变尺寸的线程池,newCachedThreadPool:

package app.executors; 
 
import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; 
 
/** 
 * Java线程:线程池 
 * 
 * @author xiho
 */
public class Test { 
  public static void main(String[] args) { 
    // 创建一个可变尺寸的线程池 
    ExecutorService pool = Executors.newCachedThreadPool();
    // 创建线程 
    Thread t1 = new MyThread(); 
    Thread t2 = new MyThread(); 
    Thread t3 = new MyThread(); 
    Thread t4 = new MyThread(); 
    Thread t5 = new MyThread(); 
    // 将线程放入池中进行执行 
    pool.execute(t1); 
    pool.execute(t2); 
    pool.execute(t3); 
    pool.execute(t4); 
    pool.execute(t5); 
    // 关闭线程池 
    pool.shutdown(); 
  } 
} 
 
class MyThread extends Thread { 
  @Override
  public void run() { 
    System.out.println(Thread.currentThread().getName() + "正在执行。。。"); 
  } 
}

pool-1-thread-1正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-4正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-5正在执行。。。
这种方式的特点是:可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们

延迟连接池,newScheduledThreadPool:

	public class TestScheduledThreadPoolExecutor {
 
  public static void main(String[] args) {
 
    ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1);
 
    exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间就触发异常
 
           @Override
 
           publicvoid run() {
 
              //throw new RuntimeException();
 
              System.out.println("================");
 
           }
 
         }, 1000, 5000, TimeUnit.MILLISECONDS);
 
    exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间打印系统时间,证明两者是互不影响的
 
           @Override
 
           publicvoid run() {
 
              System.out.println(System.nanoTime());
 
           }
 
         }, 1000, 2000, TimeUnit.MILLISECONDS);
 
  }
 
}

================

8384644549516

8386643829034

8388643830710

================

8390643851383

8392643879319

8400643939383

线程池的作用:

线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排 队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
比较重要的几个类:
ExecutorService: 真正的线程池接口。
ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

ThreadPoolExecutor的内部工作原理。

当向线程池提交一个任务之后,线程池的处理流程如下:
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务(使用threadFactory来创建)。如果核心线程池里的线程都在执行任务,则进入下个流程
2)线程池判断工作队列是否已经满了。如果工作队列没有满,则将新提交的任务存储在这个工作队列里(对应构造函数中的workQueue),则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程
3)线程池判断线程池是否已经满了(线程达到了最大数,且任务队列都满了)。如果没有,创建一个新的工作线程来执行任务。如果已经满了,任务将被拒绝并交给饱和策略来处理这个任务

线程池创建好了之后就可以向线程池里提交任务了

向线程池提交任务有两种方法,分别是execute()和submit()方法
区别是execute()方法用于提交没有返回值的任务,submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,还可以使用重载的get()方法阻塞一段时间后返回不管任务是否执行完

wait方法和sleep方法的区别。

sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep 不会释放对象锁
wait 是Object 类的方法,对此对象调用wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

1、这两个方法来自不同的类分别是Thread和Object
2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
synchronized(x){
  x.notify()
 //或者wait()
 }
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。

sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。
注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,例如t是一个线程,通过指定t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程。
wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。

2)Wait()和notify():如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。一般用在synchronized机制中。
例如:线程A
synchronized(obj) {
while(!condition) {
obj.wait();
}
obj.doSomething();

}
当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait()。在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A。
线程B
synchronized(obj) {
condition = true;
obj.notify();
}
需要注意的概念是:
1.调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {……} 代码段内(或synchronized方法中)。
2.调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {……} 代码段内唤醒A.
3.当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。
4.如果A1,A2,A3都obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。
5.obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。
6.当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。
3)为什么wait(),notify()方法要和synchronized一起使用?
因为wait()方法是通知当前线程等待并释放对象锁,notify()方法是通知等待此对象锁的线程重新获得对象锁,然而,如果没有获得对象锁,wait方法和notify方法都是没有意义的,即必须先获得对象锁,才能对对象锁进行操作,于是,才必须把notify和wait方法写到synchronized方法或是synchronized代码块中了。

什么是线程安全。

线程安全 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全 就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

到底是什么意思呢?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
好比你有两个一模一样的银行卡(账户一样,余额一样,当然现实中是没有的),假如卡上余额1000块,而你跟你女朋友同时在不同的ATM上面取1000块钱(是同时哦,理想中的同时),如果线程不安全,那么俩人都能同时取出1000块(赚死了)。而如果线程安全的话,只能一个人同时操作一个账户,当这个账户正在被操作时,是被锁起来的,不给别人动的,只能你自己动,你动完了别人才能动。

为什么会有线程安全问题?

线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
安全性:
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了

synchronized、Lock、ReentrantLock、ReadWriteLock。

https://www.cnblogs.com/dennyzhangdd/p/6925473.html

synchronized同步锁

使用:
synchronized本质上就2种锁:
1.锁同步代码块
2.锁方法
可用object.wait() object.notify()来操作线程等待唤醒
原理:synchronized细节的描述传送门: http://www.cnblogs.com/dennyzhangdd/p/6670307.html.
性能和建议:JDK6之后,在并发量不是特别大的情况下,性能中等且稳定。建议新手使用。
2.ReentrantLock可重入锁(Lock接口)
使用:ReentrantLock是Lock接口的实现类。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition来操作线程:
await()和object.wait()类似,singal()和object.notify()类似,singalAll()和object.notifyAll()类似
原理:核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
性能和建议:性能中等,建议需要手动操作线程时使用。

Synchronized与ReentrantLock的区别

https://blog.csdn.net/chenchaofuck1/article/details/51045134

volatile关键字的作用和原理。

1 保证内存可见性
1.1 基本概念
  可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
1.2 实现原理
  当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
  volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

2 禁止指令重排
2.1 基本概念
  指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。指令重排序包括编译器重排序和运行时重排序。
  在JDK1.5之后,可以使用volatile变量禁止指令重排序。针对volatile修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏
示例说明:double r = 2.1; //(1) double pi = 3.14;//(2) double area = pirr;//(3)
  虽然代码语句的定义顺序为1->2->3,但是计算顺序1->2->3与2->1->3对结果并无影响,所以编译时和运行时可以根据需要对1、2语句进行重排序。
2.2 指令重排带来的问题
如果一个操作不是原子的,就会给JVM留下重排的机会。
线程A中
{
context = loadContext();
inited = true;
}

线程B中
{
if (inited)
fun(context);
}
  如果线程A中的指令发生了重排序,那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。
2.3 禁止指令重排的原理
  volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
  JVM内存屏障插入策略:
1.每个volatile写操作的前面插入一个StoreStore屏障;
2.在每个volatile写操作的后面插入一个StoreLoad屏障;
3.在每个volatile读操作的后面插入一个LoadLoad屏障;
4.在每个volatile读操作的后面插入一个LoadStore屏障。
2.4 指令重排在双重锁定单例模式中的影响
基于双重检验的单例模式(懒汉型)
public class Singleton3 {
private static Singleton3 instance = null;

private Singleton3() {}

public static Singleton3 getInstance() {
    if (instance == null) {
        synchronized(Singleton3.class) {
            if (instance == null)
                instance = new Singleton3();// 非原子操作
        }
    }

    return instance;
}

}
instance= new Singleton()并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象 instance =memory; //3:设置instance指向刚分配的内存地址
1
2
3
上面操作2依赖于操作1,但是操作3并不依赖于操作2。所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间 instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
1
2
3
  指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
解决办法
  用volatile关键字修饰instance变量,使得instance在读、写操作前后都会插入内存屏障,避免重排序。
public class Singleton3 {
private static volatile Singleton3 instance = null;

private Singleton3() {}

public static Singleton3 getInstance() {
    if (instance == null) {
        synchronized(Singleton3.class) {
            if (instance == null)
                instance = new Singleton3();
        }
    }
    return instance;
}

}
3 适用场景
(1)volatile是轻量级同步机制。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,是一种比synchronized关键字更轻量级的同步机制。
(2)volatile无法同时保证内存可见性和原子性。加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性
(3)volatile不能修饰写入操作依赖当前值的变量。声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。
(4)当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile;
(5)volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

volatile与synchronized的区别

volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值