Java并发之多线程、Thread类常见方法以及synchronized和volatile锁关键字详解

本系列文章主要讲解Java并发相关的知识点,包括多线程、线程死锁、创建线程的方式、synchronized和volatile关键字、CAS以及锁相关内容。

致力于每位初学者都能学懂,每位学过者都能学精。

一.Java并发简介

1.概念:

并发是指在某个时间段内,多个任务交替执行。当有多个线程运行时,将CPU运行时间划分为若干个时间段,再将这些时间段分配给各个线程执行。在一个时间段的线程代码运行时,其它线程处于挂起状态。

并发与并行的区别:
并发指的是多个任务交替进行,而并行是真正意义上的同时进行。实际中,如果系统只有一个CPU,使用多线程时,在真实系统环境下不能实现并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在多个CPU的系统中。

用以下两张图片描述一下并发与并行的区别就很容易理解了
在这里插入图片描述

在这里插入图片描述
可以发现:
在单核CPU中:并发是多个任务(线程)交替执行,而任务的执行是由CPU分配的时间片来决定的。
在多核CPU中:并行是多个任务同时进行的,每个CPU执行各自任务,真正实现同时进行。

2.并发的优缺点:

优点(作用):
1).提升对CPU的使用效率。
一般来说,现在的计算机会有多个CPU核心,我们可以创建多线程,操作系统会将多线程分配给不同的CPU执行,每个CPU执行一个线程,这样就提高了CPU的使用效率。如果只使用单线程处理任务,那么就只有一个CPU工作,为了避免只让一个CPU超负荷工作,而让其他CPU空闲,我们就需要使用并发编程。

2).降低系统响应时间。
假设一个服务器同时有1000个用户访问,如果使用单线程,所有用户的请求都会进入到一个队列中排队(就像食堂只有一个打饭窗口,所有人都需要排队等候,吃饭时间效率很慢,如果多开设几个窗口,那么会大大降低打饭时间),如果我是第900个用户,那么我要等待前900个其他用户的请求都完响应了,才会给我响应,那么,用户的体验会非常差。如果使用并发多线程就可以避免过长的响应时间,用户可以轮流使用CPU资源,降低服务器响应时间。

3).提升系统的容错率。
在多线程并发操作下,各个线程的运行不受其它线程的干扰,如果某个线程运行遇到了异常,那么这个线程就抛出异常退出了,这时,其它线程可以不受影响继续执行,这样,不至于整个系统都会崩溃,提升系统容错率。

4).分工明确,执行不同策略
在我们玩单机游戏的过程中,会遇到各个"小怪",控制这些"小怪"的就是各个不同的线程,那么,当这些"小怪"遇到玩家时,会根据玩家的不同状态,而执行对应的策略,比如:进攻、抵挡、逃跑等等。如果使用单线程,那么这些"小怪"要么动作都是一样,要么,等前面一个"小怪"完成某个策略后,后面的"小怪”才会有所行动,这样,游戏的体验很差,而多线程下,各个"小怪"可以分工行动,执行不同策略,提升游戏体验度。

缺点:
1).线程不安全
我们知道:程序在多线程操作下,很可能造成线程不安全,比如:多线程下操作的HashMap、ArrayList等,因为它们的操作不是原子性的。这种情况很容易发生线程不安全。

原子性:一个任务要么完整地被执行,要么完全不执行,这种特性叫原子性。

2).造成死锁
多个线程各自拥有自己的资源,此时各个线程又想访问其它线程拥有的资源,那么,这些线程都在等待着其它线程释放资源而造成死锁。

死锁图片示例:线程A持有资源2,线程B持有资源1,线程A申请资源1,进入等待状态,线程B申请资源2,进入等待状态,造成死锁。
在这里插入图片描述

形成死锁的四个必要条件:
1.互斥条件:线程对于分配到的资源具有排他性,即一个资源只能被一个线程占用,知道被该线程释放。
2.请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:线程已获得的资源在未使用完之前不能被其它线程强行剥夺,只有自己使用完后才释放资源。
4.循环等待条件:当发生死锁时,所等待的线程一定会形成一个环路,造成永久阻塞。

如何避免?
破坏四个条件中的一个,即:
1.破坏互斥条件:无法破坏,本身用锁就是为了使它们互斥(临界资源需要互斥访问)。
2.破坏请求与保持条件
一次申请完所有需要的资源。
3.破坏不剥夺条件
占用部分资源的线程申请其它资源时,如果申请不到,可以主动释放它已经占用的资源。
4.破坏循环等待条件
按某一顺序申请资源,释放资源时相反,可以破坏循环等待条件。

3).上下文切换造成时间的占用浪费
多个线程执行不同任务时,如果频繁交替,则一定会增加上下文切换的时间,造成系统执行时间过长,影响效率。

上下文切换:当发生任务切换时,保存当前任务的寄存器到内存中,将下一个即将要切换过来的任务的寄存器状态恢复到当前CPU寄存器中,使其执行,同一时刻只允许一个任务独享寄存器。

3.如何保证多线程的安全性?

原子性:执行时需要保证原子性
可见性:一个线程对共享变量的修改,其它线程可以看到。(synchronized,volatile)
有序性:程序执行的顺序按照代码的先后顺序执行(可能有重排序)。

出现线程安全问题的原因:
线程切换带来原子性问题
缓存导致可见性问题
编译优化带来的有序性问题

解决方法:
使用synchronized、Lock解决原子性问题
使用synchronized、volatile、Lock解决可见性问题
happens-before规则解决有序性问题

什么是进程?
进程指正在运行中的程序。每个进程都有自己独立的地址空间(内存空间),每当用户启动一个进程时,操作系统就会为该进程分配一个独立的内存空间,让应用程序在这个独立的内存空间运行。例如,一个exe就是一个进程程序。

什么是线程?
线程是一个轻量级的子进程,是最小的处理单元,线程是进程的子集。线程是独立的,如果一个线程在执行中发生异常,则不会影响其它线程,它使用共享内存区域。

进程和线程的区别:
线程在共享的内存中运行,进程在不同的内存空间中运行
线程可以使用wait(),notify(),notifyAll()等方法直接与其它线程通信,而进程需要使用"IPC"来与其它进程通信

IPC:进程间通信,是指在不同进程之间传播或交换信息。
IPC的方式通常有管道、消息队列、信号量、共享存储、Socket、Streams等。其中Socket和Streams支持不同主机上的两个进程IPC

线程和进程关系图:
在这里插入图片描述

二.Thread类的常见方法以及实例演示

前面介绍的主要是并发编程的相关概念。那么,接下来,我们通过实例演示,让大家彻底理解并发编程之多线程相关的内容。

1.创建线程的四种方式:
1).继承Thread类
2).实现Runnable接口
3).实现Callable接口
4).使用Executors工具类创建线程池

先来看看Thread类相关知识点和常用方法
1.设置线程名
使用多线程时,我们可以通过调用Thread.currentThread().getName()查看线程名。若不做设置,则默认线程名的格式如下:主线程为main,其它线程为Thread-x
具体是如何命名的,先看一下命名的源码:

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
// nextThreadNum 同步方法,线程安全,不会出现重复的threadInitNumber
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

构造方法中的init()方法中有nextThreadNum()方法,nextThreadNum()是定义及返回线程初始化的数量threadInitNumber。

再看init()方法:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

初始化init()方法中我们发现:初始化时,如果没有设置name值,那么就使用默认定义的命名方法“Thread-threadInitNumber”。

如果我们想要给线程取名,我们可以用以下构造方法的Thread(Runnable target, String name) 方法,第二个参数传需要取名的字符串就可以。

//传入Runnable接口实现
Thread(Runnable target)
//传入Runnable接口实现,传入线程名
Thread(Runnable target, String name) 
//设置当前线程用户组
Thread(ThreadGroup group, Runnable target)
//设置用户组,传入线程名
Thread(ThreadGroup group, Runnable target, String name)
//设置用户组,传入线程名,设置当前线程栈大小
Thread(ThreadGroup group, Runnable target, String name, long stackSize) 

举例通过继承Thread类方法设置线程名:

public class ThreadName  extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
public class ThreadNameTest {
    public static void main(String[] args){
        ThreadName threadName=new ThreadName();
        //使用带参构造方法Thread(Runnable target, String name)来起线程名字
        Thread thread1=new Thread(threadName,"线程1");
        Thread thread2=new Thread(threadName,"线程2");
        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName());
    }
}

结果:在这里插入图片描述如果不设置线程名:

public class ThreadNameTest {
    public static void main(String[] args){
        ThreadName threadName1=new ThreadName();
        ThreadName threadName2=new ThreadName();
        threadName1.start();
        threadName2.start();
        System.out.println(Thread.currentThread().getName());
    }
}

结果:
在这里插入图片描述

public class ThreadNameTest {
    public static void main(String[] args){
        ThreadName threadName=new ThreadName();
        //使用带参构造方法Thread(Runnable target, String name)来起线程名字
        Thread thread1=new Thread(threadName);
        Thread thread2=new Thread(threadName);
        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName());
    }
}

结果:
在这里插入图片描述
2.设置守护线程
守护线程是一个服务线程,当其它用户线程执行完,JVM退出,守护线程也就被停止。其中垃圾回收线程就是守护线程。

注意:
1.设置守护线程必须在线程启动前
2.设置守护线程不要访问共享资源,防止守护线程异常退出。
3.守护线程中产生的新线程也是守护线程。

为何设置守护线程必须在线程执行前设置?

public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        // daemon属性默认为false
        daemon = on;
    }

通过源码可知,如果线程启动了,则抛出非法线程状态的异常。

举例设置守护线程:

public class ThreadNameTest {
    public static void main(String[] args){
        ThreadName threadName=new ThreadName();
        Thread thread1=new Thread(threadName,"线程1");
        Thread thread2=new Thread(threadName,"线程2");
        thread1.setDaemon(true);
        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName());
    }

如果线程1设置为守护线程,线程2和主线程执行完,守护线程就不执行了。

3.isAlive()方法

public final native boolean isAlive();

判断线程是否存活,如果存活返回true,否则返回false。
举例:

public class ThreadNameTest {
    public static void main(String[] args){
        ThreadName threadName=new ThreadName();
        //使用带参构造方法Thread(Runnable target, String name)来起线程名字
        Thread thread1=new Thread(threadName,"线程1");
        Thread thread2=new Thread(threadName,"线程2");
        Thread thread3=new Thread(threadName,"线程3");
        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName());
        System.out.println("线程1是否存活?"+thread1.isAlive());
        System.out.println("线程3是否存活?"+thread3.isAlive());
    }
}

结果:在这里插入图片描述
由结果可知:创建一个线程3但没有启动,通过判断可以发现此线程并不存活。

4.sleep()方法

public static native void sleep(long millis) throws InterruptedException;

可以看出:sleep()方法是本地方法,作用是暂停当前线程,把cpu片段让出给其他线程,减缓当前线程的执行。
举例:

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Process process = new Process();
        Thread thread = new Thread(process);
        thread.setName("线程Process");
        thread.start();
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
            //阻塞main线程,休眠一秒钟
            Thread.sleep(1000);
        }
    }
}
class Process implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

结果:
在这里插入图片描述
从结果很容易得知:main线程运行后,休眠1s,交出CPU使用权,此时其它线程在main线程休眠期间执行当前线程任务。

步骤:
1.main线程执行。
2.main线程休眠1s,在休眠期间交出CPU使用权,其它线程执行。
3.若main线程休眠期间其它线程能够执行完,则main线程唤醒后,没有其它线程执行。
4.若main线程休眠期间其它线程没有执行完,则main线程唤醒后,会先执行main线程,然后再执行其它线程。

5.yield()方法
Java线程中的yield( )方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,但不是单纯的让给其他线程。

yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

举例:

public class ThreadYieldTest {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.setPriority(Thread.MAX_PRIORITY);
        t.start();
        for (int i = 0; i < 3; i++) {
            Thread.yield();
            System.out.println(Thread.currentThread().getName());
        }
    }
}
class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 3; i++)
            System.out.println(Thread.currentThread().getName());
    }
}

结果1:主线程让出CPU,其它线程获取CPU并执行完
在这里插入图片描述
从结果可知:执行主线程时,调用yield()方法,主线程此时让出CPU执行权,且这时,其它线程获得了CPU执行权,并执行完,然后主线程再获取CPU执行完剩余任务。这只是一种情况,还有一种情况是,主线程让出CPU后,又通过竞争获得了CPU执行权继续执行完。

结果2:主线程让出CPU后,又通过竞争获取到CPU执行权,并执行完:
在这里插入图片描述

6.join方法**
调用join方法的线程,会等待该线程执行完后,再执行其它线程。

//Thread类中
public final void join() throws InterruptedException {
    join(0);
}


public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();  //获取当前时间
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {    //这个分支是无限期等待直到b线程结束
        while (isAlive()) {
            wait(0);
        }
    } else {    //这个分支是等待固定时间,如果b没结束,那么就不等待了。
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

举例:不加join()方法

public class ThreadJoinTest {
    public static void main(String[] args) throws InterruptedException {
        MyThreadTest myThreadTest = new MyThreadTest();
        Thread thread = new Thread(myThreadTest);
        thread.setName("MyThreadTest");
        thread.start();
        System.out.println(Thread.currentThread().getName() +"执行" );
        /*try {
            thread.join();    //调用join()
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        System.out.println("main线程执行完毕");
    }
}
class MyThreadTest implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行结束");
    }
}

结果:
在这里插入图片描述
从结果可知:不使用join()方法,main线程会先执行完,其它线程再执行完。

加join()方法:

public class ThreadJoinTest {
    public static void main(String[] args) throws InterruptedException {
        MyThreadTest myThreadTest = new MyThreadTest();
        Thread thread = new Thread(myThreadTest);
        thread.setName("MyThreadTest");
        thread.start();
        System.out.println(Thread.currentThread().getName() +"执行" );
        try {
            thread.join();    //调用join()
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main线程执行完毕");
    }
}
class MyThreadTest implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行结束");
    }
}

结果:

在这里插入图片描述
从结果可知:其它线程使用join()方法时,其它线程会先执行完成,main线程等待其它线程执行完才执行。

7.interrupt()方法
interrupt()是用来中断一个线程的方法。

首先,先了解一下Thread中的两个方法:interrupted 和 isInterrupted,另外还提供了获取中断标志位的接口。

private native boolean isInterrupted(boolean ClearInterrupted);

这是一个native方法,同时也是一个private方法,该方法除了能够返回当前线程的中断状态,还能根据ClearInterrupted参数来决定要不要重置中断标志位。

public boolean isInterrupted() {
    return isInterrupted(false);
}

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

其中isInterrupted调用了isInterrupted(false), ClearInterrupted参数为false, 说明它仅仅返回线程实例的中断状态,但是不会对现有的中断状态做任何改变

interrupted和isInterrupted的区别:
interrupted:是静态方法,查看当前中断信号是true还是false,并且清除中断信号。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面就返回false了(清除了中断标志位)。
isInterrupted:查看当前中断信号是true还是false,且不会清除中断标志位。

interrupt源码:

public void interrupt() {
    if (this != Thread.currentThread())//检查是否有权限
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {//判断是否是阻塞线程调用(如sleep)
            interrupt0(); // Just to set the interrupt flag
            b.interrupt(this);//如果是阻塞线程调用interrupt,抛出异常,将中断标志位改为false
            return;
        }
    }
    interrupt0();//修改到标志位
}

总结下步骤为:
1.检查是否有权限
2.判断是否是阻塞线程调用interrupt()方法
3.如果是阻塞线程调用,则抛出异常且将中断标志位改为false
4.如果不是阻塞线程调用,那么直接修改中断标志位

注意:interrupt不会真正停止一个线程,它仅仅给这个线程发送信号,设置一个中断标志,这个标志可以给我们判断什么时候做什么动作以及告诉这个线程需要被中断,且需要线程自己来终止。

举例:

public class ThreadInterruptTest extends Thread {
    @Override
    public  void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("i="+i);
            if(this.isInterrupted()){//当前线程t
                System.out.println("检测当前线程是否中断");
                System.out.println("第一个interrupted()"+this.interrupted());//检测当前中断信号并清除
                System.out.println("第二个interrupted()"+this.interrupted());//此时中断信号已经清除
                break;
            }
        }
        System.out.println("检测到中断,跳出循环,线程到这里结束。");
    }
}
class TestDemo {
    public static void main(String[] args ) throws InterruptedException {
        ThreadInterruptTest t =new ThreadInterruptTest();
        t.start();
        //中断t线程
        t.interrupt();
        //sleep等待一秒,等测试线程运行完
        Thread.currentThread().sleep(1000);
        System.out.println("测试线程是否存活:"+t.isAlive());
    }
}

结果:
在这里插入图片描述

通过结果可知:原本需要打印0~5的整数时,由于使用interrupt()方法,导致打印中断,只能打印第一个数字,最后跳出循环。

如果不打断结果如何呢?

public class ThreadInterruptTest extends Thread {
    @Override
    public  void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("i="+i);
            if(this.isInterrupted()){//当前线程t
                System.out.println("检测当前线程是否中断");
                System.out.println("第一个interrupted()"+this.interrupted());//检测当前中断信号并清除
                System.out.println("第二个interrupted()"+this.interrupted());//此时中断信号已经清除
                break;
            }
        }
        System.out.println("检测到中断,跳出循环,线程到这里结束。");
    }
}
class TestDemo {
    public static void main(String[] args ) throws InterruptedException {
        ThreadInterruptTest t =new ThreadInterruptTest();
        t.start();
        System.out.println("测试线程是否存活:"+t.isAlive());
        //中断t线程
        //t.interrupt();
        //sleep等待一秒,等测试线程运行完
        Thread.currentThread().sleep(1000);
        System.out.println("测试线程是否存活:"+t.isAlive());
    }
}

结果:
在这里插入图片描述

由结果可知:不使用interrupt()方法,线程会循环打印完后自动退出。

三.synchronized、volatile以及Lock的简单介绍

了解了Thread类的常用方法后,我们还需要对多线程中的一些关键字和相关类有所了解,学习这些关键字和类可以让我们了解它们是如何使得多线程变为线程安全的。

1.synchronized介绍
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
synchronized关键字可以修饰类、变量以及方法。

synchronized的三种使用方式:
1.修饰实例方法
对当前对象实例加锁,进入同步代码块前需要获得实例对象的锁。

2.修饰静态方法
对整个类加锁,作用于类的所有实例对象,如果线程A调用一个实例对象的非静态synchronized方法,线程B调用这个类的静态synchronized方法,那么不会发生互斥现象,因为静态synchronized方法占用的锁是当前类的锁,而非静态synchronized方法占用的是当前实例对象的锁。

3.修饰代码块
指定加锁对象,进入同步代码块前需要获得指定对象的锁。

什么是单例模式
单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。

作用:类似于打印机,如果多个人同时打印一份资料,那么打印的结果肯定很乱,而单例模式只需要一个人来打印资料,避免多人的干扰。

双重校验锁实现对象单例(线程安全)

public class Single{ 
    private volatile static Single Instance;
    private Single(){ 
    }
    public static Single getInstance(){
        //先判断对象是否实例化,没有实例化才进入加锁
        if(Instance==null){
           //类对象加锁
           synchronized(Single.class){
	       if(Instance==null){
		  Instance=new Single();
		}
	   }
	}
	return Instance;
    }
}

为何需要判断两次Instance==null(是否实例化)?
1.第一次判断是防止每次调用getInstance()时都会加锁,频繁加锁会影响效率,我们只需要未被实例化的对象,而已经被实例化的对象直接排除。
2.第二次加锁的目的是防止多次实例化,因为在进入第一个判断if时可能有多个线程,假设有线程A和线程B都进入了第一个if判断语句,此时线程A可能先获取对象锁,如果此时没有第二个判断,那么线程A先实例化对象,释放锁,然后线程B获取对象锁,再次实例化,此时已经实例化两个了,显然不符合要求,如果加了第二个判断,那么,线程A实例化对象后,线程B会先判断是否实例化,如果实例化了,就不再实例化。

2.volatile介绍
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

并发编程的三个基本概念
1 原子性
定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。
Java中的原子性操作包括:
基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。所有引用reference的赋值操作java.concurrent.Atomic.* 包中所有类的一切操作。

2 可见性
定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3 有序性
定义:即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile变量的特性
1.保证可见性,不保证原子性
可见性:当某个线程修改volatile变量时,JMM会强制将这个修改更新到主内存中,并且让其他线程工作内存中存储的副本失效。volatile变量并不能保证其操作的原子性,具体来说像i++这种操作并不是原子操作,使用volatile修饰变量后仍然不能保证这一点。

2.禁止指令重排
重排序操作不会对存在数据依赖关系的操作进行重排序。
重排序是为了优化性能,单线程下程序的执行结果不能被改变。

3.ReentrantLock介绍
ReentrantLock是可重入锁。
ReentrantLock的主要用法:

Lock lock = new ReentranLock();
lock.lock();
try{
    //do something
}finally{
    lock.unlock();
}

从示例可以看出,ReentrantLock和synchronized是有区别的:
1.synchronized是关键字,而ReentrantLock是类
2.ReentrantLock使用灵活,但是需要有释放锁的动作
3.ReentrantLock必须手动释放锁,而synchronized不需要手动释放锁
4.ReentrantLock适用于代码块锁,而synchronized可以修饰类、方法和变量等。

公平锁和非公平锁
公平锁:如果一个锁是公平的,那么获取锁的顺序就应该符合请求上的绝对时间顺序,满足FIFO
非公平锁:只要有机会就尝试抢占资源,是一种竞争关系,ReentrantLock默认是非公平锁。
Lock lock = new ReentranLock(false);//非公平锁
Lock lock = new ReentranLock();//非公平锁
Lock lock = new ReentranLock(true);//公平锁
参数是false或者不传参都是非公平锁,参数传true则是公平锁。

1.公平锁能保证:旧的线程排队使用锁,新线程仍然排队使用锁。
2.非公平锁保证:旧的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。

优点:
非公平锁性能高于公平锁性能,非公平锁更能充分利用cpu的时间片,尽量减少CPU空闲状态时间。
公平锁可以解决线程饥饿。

缺点:
非公平可能导致后面排队等待的线程等不到相应的CPU资源,从而引起线程饥饿。
公平锁的性能较低,CPU空闲时间可能多于非公平锁。

以上为Java并发相关的一些基础知识点,关于并发关键字锁以及CAS后面会详细讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值