Java中的线程--对线程来个多方位理解!

Posted SSimeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的线程--对线程来个多方位理解!相关的知识,希望对你有一定的参考价值。

Java中的线程总结

一、线程的生命周期

1. 进程与线程的定义

(1) 定义:可以简单的理解进程与线程。可以把进程看成一项具体的任务,而线程是为了完成该任务来分解的子任务。
(2) 比如小明今天下5点要去理发店剪头发,这是一项任务(进程),理发店的人可能比较多,因此理发的环节有洗、剪、吹。这每一个环节就是子任务(线程)。
(3) 多线程可以提高效率。

在这里插入图片描述

2. 进程与线程的特征

(1)线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
(2)一个进程由一个或多个线程组成,线程是一个进程中的代码不同执行路线。
(3)进程之间相互独立,同一进程下的多个线程共享程序的内存空间以及资源。
(4)线程也被认为是轻量级的进程。

3. 线程的生命周期

线程的生命周期一共有5个阶段:创建、就绪、运行、阻塞、自然死亡。
(1)创建有3种方式(二中有解释)
(2)就绪:start();此时并没有执行,就绪不一定CPU(抢占式)会执行
(3)运行:run()和call()
(4)阻塞(block)(4种):

  1. sleep()
    自己休眠一定时间,释放资源,让其他资源去抢占。当休眠时间过后,变为就绪状态,重新去抢占CPU资源。

  2. yield()
    自己释放资源后,立马变成就绪状态去抢夺资源。

  3. join()
    直到调用join方法的线程执行完毕才能访问资源。

  4. wait()
    只能在synchronized语句中使用。当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程

(5)自然死亡状态(不包括意外死亡)
在这里插入图片描述

二、创建线程的3种方式

1.继承Thread类

继承Thread类来创建线程,然后重写Thread类里面的run()方法。

package simeng.thread;

public class ThreadDemo01 {
    //此处的main也是一个线程哦,叫做主线程!
	public static void main(String[] args) {
		Thread t1=new MyThread();
		Thread t2=new MyThread();
		Thread t3=new MyThread();
		t1.start();
		t2.start();
		t3.start();
		
	}
	/**
	 * 自定义的线程类,完成1到10的打印
	 * @author Ssimeng
	 *
	 */
	public static class MyThread extends Thread{
		@Override
		public void run() {
			for(int i=0;i<10;i++) {
				System.out.println(Thread.currentThread().getName()+": "+i);
				try {
					Thread.sleep(200);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

}

结论:线程是抢占式的,处在runnable状态下的线程,谁抢到资源谁就可以执行,因此顺序是无法确定的。
执行结果:
在这里插入图片描述

2.实现Runnable接口

2.实现Runnable接口来创建线程,然后重写run()方法。

package simeng.thread;

public class ThreadDemo02 {
	public static void main(String[] args) {
		Thread t1=new Thread(new MyThread2());
		Thread t2=new Thread(new MyThread2());
		Thread t3=new Thread(new MyThread2());
		t1.start();
		t2.start();
		t3.start();
	}
	/**
	 * 采用实现Runnable接口的方式自定义线程
	 * 功能依旧是打印1到10,与继承Thread类的效果一样
	 * @author SSimeng
	 *
	 */
	public static class MyThread2 implements Runnable{
		@Override
		public void run() {
			for(int i=0;i<10;i++) {
				System.out.println(Thread.currentThread().getName()+": "+i);
				try {
					Thread.sleep(200);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

}

3.实现Callable接口

3.实现Callable接口来创建线程,然后重写call()方法。

package simeng.newthread;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadDemo03 {
	
	public static void main(String[] args) throws Exception {
		//FutureTask实现了Runnable接口
		Thread t1=new Thread(new FutureTask<Integer>(new Mythread03()));
		Thread t2=new Thread(new FutureTask<Integer>(new Mythread03()));
		Thread t3=new Thread(new FutureTask<Integer>(new Mythread03()));
		t1.start();
		t2.start();
		t3.start();
	}
	/**
	 * 通过实现Callable接口,自定义异常
	 * 功能:求1到100的累加
	 * @author Ssimeng
	 *
	 */
	public static class Mythread03 implements Callable<Integer>{
		
		@Override
		public Integer call() throws Exception {
			int s=0;
			for(int i=0;i<=100;i++) {
				s+=i;
				System.out.println(Thread.currentThread().getName()+":"+s);
				Thread.sleep(200);
			}
			System.out.println(Thread.currentThread().getName()+":总和为: "+s);
			return s;
		}
	}
}

代码执行结果:
在这里插入图片描述

Callable方式与其他两种方式的区别

区别:Thread类和Runnable中的重写的都是run方法,run方法没有返回值还不可以抛异常
而Callable接口重写的是call方法,而call方法是可以有返回值的并且可以抛异常。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

三、ABA问题与CAS原理

1.CAS原理

CAS(Compare And Swap),即比较然后交换。是用于实现多线程同步的原子指令,它将内存位置的内容(V)与预期原值(A)相比较,相同则修改内存位置的值为新值(B),否则处理器不做任何操作。

2. ABA问题

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,被其它线程变成了B,然后又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是ABA问题。

代码模拟ABA问题:

0线程将x从10改到20,然后再从20改回10。
1线程将x从10改为999的时候,按道理应该是失败的,但是由于CAS比较的是内存的值以及预期值,而0线程操作后,预期值大小相等,1线程就给重新赋值了。这就是ABA问题,A经过变换后又变为了A但是却赋值成功了。

package simeng.aba;

import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo {
	private static AtomicInteger x = new AtomicInteger(10);

	public static void main(String[] args) {
		// 创建的第一个线程
		new Thread(() -> {
			System.out.println("当前线程名:" + Thread.currentThread().getName()  +" "+ x.compareAndSet(10, 20)+" "+ x.get());// 将10改为20
			System.out.println("当前线程名:" + Thread.currentThread().getName()  +" "+ x.compareAndSet(20, 10) +" "+ x.get());// 将20改为10
			
		}).start();
		
		// 创建的第二个线程
		new Thread(() -> {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("当前线程名:" + Thread.currentThread().getName() +" "+ x.compareAndSet(10, 999)+" "+ x.get());// 将10改为999
			
		}).start();

	}

}

代码执行结果:
在这里插入图片描述

3 ABA问题的解决思路

使用AtomicStampedReference类,AtomicStampedReference类引入了版本概念,每次进行CAS操作使,只有当同时满足CAS的“期望值正确匹配”“版本号正确匹配”,才能正确CAS。

四、volatile与synchronized关键字以及lock

1.volatile关键字

这个关键字是在多个线程共享一个资源时解决线程的可见性有序性的问题的。

(1)可见性

(1) 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
(2)Java内存模型规定了所有的变量都存储在主内存中,每条线程在自己的工作内存,线程的工作内存中保存了该线程中主内存变量拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
(3)不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次使用之前都从主内存刷新。
在这里插入图片描述

(2)有序性

普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。

时序性:
即程序执行的顺序按照代码的先后顺序执行。除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,
比如load->read->save 有可能被优化成load->save->read ,这就是可能存在有序性问题。

volatile关键字 volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行,load->add->save 的执行顺序就是:load、add、save。

2.synchronized关键字

又称为同步锁。当本线程在执行synchronized代码块时,其他线程在上锁的代码块执行完成后才能访问这些代码块,但是可以访问其他没有上锁的代码块。同一时间只有一个对象能获得对象的锁。

两种使用方式(后面会有卖票示例):
一种是锁定方法

	public synchronized void test(){
	     int i=5;
	     while(i>0){
	       i--;
	     }
	     System.out.println(i);
	}

一种是锁定代码块。

	public void test(){
	     int i=5;
	     synchronized(this){
	     	while(i>0)
	           i--;
	     }
	     
	     System.out.println(i);
	}

3.ReentrantLock锁

Lock 是一个接口,提供无条件、可轮训的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显示的。

synchronized锁与ReentrantLock的对比:
(1)synchronized不需要手动释放和开启,对象之间是互斥关系。
(2)使用灵活,但必须有手动开启锁和释放锁的操作,只适应于代码块。

五、AtomicInteger与int的区别

int是线程不安全的,在做自增运算时不满足线程的原子性。
而AtomicInteger在高并发的情况下是线程安全的。
注意:AtomicInteger类只能保证在自增或者自减的情况下保证线程安全

六、线程的三大特性

1.原子性

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在JAVA中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

2.可见性

先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。因此需要使用volatile关键字来满足线程的可见性,将线程的工作内存的资源刷新到主内存中。

3.有序性

程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

七、死锁的四个必要条件

1、互斥

某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。

2、占有且等待

一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。

3、不可抢占

别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。

4、循环等待

存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。

死锁示例:
A锁住自己的资源问B要,B锁住自己的资源要C,C锁住自己的资源问A要。这就导致了三个线程谁都不能满足自己的需求,成了死循环,也就构成了死锁。
在这里插入图片描述
代码模拟:

package simeng.aba;

public class DeadLockDemo {
	public static void main(String[] args) {
		DeadLock a=new DeadLock("A");
		DeadLock b=new DeadLock("B");
		DeadLock c=new DeadLock("C");
		Thread t1=new DeadLockThread(a, b, c);
		Thread t2=new DeadLockThread(b, c, a);
		Thread t3=new DeadLockThread(c, a, b);
		t1.start();
		t2.start();
		t3.start();
		
	}
	public static class DeadLock{
		private String name;//锁的名字
		
		
		public DeadLock(String name) {
			this.name = name;
		}


		public String getName() {
			return name;
		}

		
		
	}
	public static class DeadLockThread extends Thread{
		private DeadLock a;
		private DeadLock b;
		private DeadLock c;
		
		public DeadLockThread(DeadLock a, DeadLock b, DeadLock c) {
			this.a = a;
			this.b = b;
			this.c = c;
		}

		@Override
		public void run() {
			synchronized (a) {
				System.out.println(a.getName()+"资源已被锁住");
				System.out.println("准备锁"+b.getName()+"资源");
				synchronized (b) {
					System.out.println(b.getName()+"资源已被锁住");
				}
				System.out.println(b.getName()+"资源已被释放");
			}
			System.out.println(a.getName()+"资源已被释放");
			synchronized (b) {
				System.out.println(b.getName()+"资源已被锁住");
				System.out.println("准备锁"+c.getName()+"资源");
				synchronized (c) {
					System.out.println(c.getName()+"资源已被锁住");
				}
				System.out.println(c.getName()+"资源已被释放");
			}
			System.out.println(b.getName()+"资源已被释放");
			synchronized (c) {
				System.out.println(c.getName()+"资源已被锁住");
				System.out.println("准备锁"+a.getName()+"资源");
				synchronized (a) {
					System.out.println(a.getName()+"资源已被锁住");
				}
				System.out.println(a.getName()+"资源已被释放");
			}
			System.out.println(c.getName()+"资源已被释放");
		}
	}

}

程序执行结果:一直在死锁没有停止
在这里插入图片描述

八、卖票示例

1.用synchronized和AtomicInteger以及volatile

package simeng.aba;

import java.util.concurrent.atomic.AtomicInteger;
public class TicketDemo {

	public static void main(String[] args) {
		Ticket ticket = new Ticket();
		TicketThread a = new TicketThread(ticket, "A:");
		TicketThread b = new TicketThread(ticket, "B:");
		TicketThread c = new TicketThread(ticket, "C:");
		TicketThread d = new TicketThread(ticket, "D:");
		a以上是关于Java中的线程--对线程来个多方位理解!的主要内容,如果未能解决你的问题,请参考以下文章

Android Java 线程池 ThreadPoolExecutor源代码篇

JAVA 对守护线程的理解

Java中通过Runnable与Thread创建线程的区别

多个用户访问同一段代码

如何理解javascript中的同步和异步

线程池的设计原理是什么?