线程的定义

线程是操作系统调度的最小单元,也叫轻量级进程。它被包含在进程之中,是进程中的实际运作单位。同一进程可以创建多个线程,每个进程都有自己独立的一块内存空间。并且能够访问共享的内存变量。

线程包含的五种状态

1、新建状态(New): 线程对象被创建后,就进入了新建状态。例如: Thread thread = new Thread().

2、就绪状态(Runnable): 也被成为“可执行状态”。线程对象被创建后,其他线程调用了该对象的start()方法,从而来启动该线程。例如: thread.start(). 出于就绪状态的线程,随时可能被CPU调度执行。

3、运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

4、阻塞状态(Blocked): 阻塞状态是现场称因为某种原因放弃CPU使用权,暂时停止运行,知道线程进入就绪状态,才有机会转到运行状态。阻塞状态情况分为三种:

​ 1、等待阻塞–通过调用线程的wait()方法,让线程等待某工作的完成。

​ 2、同步阻塞–线程在获取synchronized同步锁失败(因为锁被其他线程所专用),它会进入同步阻塞状态。

​ 3、其他阻塞–通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时,join等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead): 线程执行完了或者因一场退出了run()方法,该线程结束生命周期。

什么是线程安全和线程不安全

假如A与B同时去取公司账户的资金,如果线程不安全,那么A与B同时取一万块,,银行亏了,而如果线程安全呢?就只能有一个人能取出来一万块。

线程安全就是多个线程在同时执行一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,不错在执行结果的非议性。

线程不安全就是不提供加锁保护机制,有可能出现多线程先后更改数据造成所得到的的数据是脏数据。

有哪些解决线程并发安全

1、同步代码块

> synchronized(){
> 
> }
> ```
>
> 2、同步方法
>
> ```java
> public synchronized void test(){
>   // synchronized必须放在void之前
> }
> ```
>
> 3、锁机制
>
> ```java
> public class Demo {
>     private static Lock lock = new ReentrantLock();
> 
>     public static void main(String[] args) {
>         new Thread(() -> test(),"线程A").start();
>         new Thread(() -> test(),"线程B").start();
>     }
>     public static void test(){
>         try {
>             lock.lock();
>             System.out.println(Thread.currentThread().getName() + "获取了锁");
>             Thread.sleep(2000);
>         } catch (InterruptedException e) {
>             e.printStackTrace();
>         } finally {
>             System.out.println(Thread.currentThread().getName() + "释放了锁");
>             lock.unlock();
>         }
>     }
> }
> ```
>
> 

### 锁的机制有哪些分类

#### 1、乐观锁 VS 悲观锁

> ​ 乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。
>
> 先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
>
> 而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

​       根据从上面的概念描述我们可以发现:

- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

java // 悲观锁synchronized调用 public synchronized void testMethod(){ // 同步操作资源 } // 悲观锁ReentrantLock调用 private ReentrantLock lock = new ReentrantLock(); public void modifyPublicResources(){ lock,lock(); // 操作同步资源 lock.unlock(); }

// 乐观锁的调用方式 java.util.concurrent.atomic中的类可以分成4组: 标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater 复合变量类:AtomicMarkableReference,AtomicStampedReference


##### 标量类

java //获取当前的值 public final int get() //取当前的值,并设置新的值 public final int getAndSet(int newValue) //获取当前的值,并自增 public final int getAndIncrement() //获取当前的值,并自减 public final int getAndDecrement() //获取当前的值,并加上预期的值 public final int getAndAdd(int delta) – 演示 import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerTest { static AtomicInteger ai = new AtomicInteger(1); public static void main(String[] args) { System.out.println(ai.getAndIncrement()); System.out.println(ai.get()); } }


> Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe实现的.
>
> 发现Unsafe只提供了三种CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现其是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新double也可以用类似的思路来实现。

java public class AtomicIntegerArrayTest { static int[] value = new int[] { 1, 2 }; static AtomicIntegerArray ai = new AtomicIntegerArray(value); public static void main(String[] args) { ai.getAndSet(0, 3); System.out.println(ai.get(0)); System.out.println(value[0]); } }


> AtomicIntegerArray类需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响到传入的数组。

##### 原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子的更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下三个类:

- AtomicReference:原子更新引用类型。
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
- AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

java import java.util.concurrent.atomic.AtomicReference;

public class run { public static AtomicReference atomicUserRef = new AtomicReference();

public static void main(String[] args) { User user = new User(“conan”, 15); atomicUserRef.set(user); User updateUser = new User(“Shinichi”, 17); atomicUserRef.compareAndSet(user, updateUser); System.out.println(atomicUserRef.get().getName()); System.out.println(atomicUserRef.get().getOld()); }

static class User { private String name; private int old;

public User(String name, int old) { this.name = name; this.old = old; }

public String getName() { return name; }

public int getOld() { return old; } } }


##### 原子更新字段类
如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类,Atomic包提供了以下三个类:

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。

原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个更新器。原子更新类的字段的必须使用public volatile修饰符。

java import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class run {

private static AtomicIntegerFieldUpdater a = AtomicIntegerFieldUpdater .newUpdater(User.class, “old”);

public static void main(String[] args) { User conan = new User(“conan”, 10); System.out.println(a.getAndIncrement(conan)); System.out.println(a.get(conan)); }

public static class User { private String name; public volatile int old;

public User(String name, int old) { this.name = name; this.old = old; }

public String getName() { return name; }

public int getOld() { return old; } } } ```

2、自旋锁 VS 适应性自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。