2020-09-16 Java 多线程及线程安全

Java多线程

进程:一个程序的执行过程叫进程
线程:进程的一条执行路径
多线程:进程的多条执行路径

线程的五种状态

实现方式之一

声明一个类,继承Thread类,重写run()方法,调用类的实例对象的start()方法。

public class Test extends Thread{
		@Override
		public void run (){
			//线程执行代码块
	}
}

//main方法
main(arg){
	new Test().start();	//线程启动 ,如果调用的是run()方法,则没有多线程效果。
	//中间可写主线程需要执行的代码
}

实现方式之二

声明一个类,实现Runnable接口,也需要重写run()方法,将类的实例对象通过参数传入Thread类的实例对象里,然后调用Thread类实例对象的start()方法。

public class Test implements Runnable{
		@Override
		public void run(){
			//需要执行的代码块
		}
}

main(arg){
		Test t=new Test();
		Thread th=new Thread(t);
		th.start();   //线程启动
		//主线程执行代码块
}

解决线程安全问题

由于CPU切换线程快速,多个线程使用共享资源时会造成“异常”场景或情况,这就是线程安全问题。

可以给会出现异常的代码块用同步锁修饰(Synchronized),而且需要指定同一把锁。任何对象都可以作为锁,同一把锁表示同一个对象或常量。同步锁只能同步方法和代码块。

被Synchronized修饰的代码块,只有代码块执行完毕后才会释放锁,另一个线程才能拿到锁进入代码块。

锁对象

什么是锁对象?

每个java对象都有一个锁对象

如何创建锁对象:
每个java对象都可以作为锁,可以使用this关键字作为锁对象,也可以使用所在类的字节码文件对应的Class对象作为锁对象

类名.class
对象.getClass()

Java中的每个对象都有一个内置锁,只有当对象具有同步方法或同步代码块时,内置锁才会起作用,当进入一个同步的非静态方法时,就会自动获得与类的当前实例(this)相关的锁。获得一个对象的锁也称为获取锁。

因为一个对象只有一个锁,所以如果一个线程获得了这个锁,使用这个锁的其他线程就不能获得了,直到这个线程释放(或者返回)锁。

1:只能同步方法(代码块),不能同步变量或者类

2:每个对象只有一个锁

3:类可以同时具有同步方法和非同步方法,不用同步的方法不要加锁。

4:一个线程获得了锁,其他线程就不可以进入该锁对应相关的同步方法或同步代码块。

5:如果类同时具有同步方法和非同步方法,那么多个线程仍然可以访问该类的非同步方法。同步会影响性能、甚至死锁(两个线程相互拿着另一个线程所需要的资源不释放形成死锁。),同步优先考虑同步代码块(作用范围更小、更灵活)。

6:如果线程进入sleep() 睡眠状态,该线程会继续持有锁,不会释放。

线程的通讯

线程间也需要通信,一种方式是多个线程通过共享资源,来进行数据的交换,另一种是线程间可以通过对应的方法进行通讯。

Synchronized能解决线程安全问题,但是不能解决存一次,取一次的问题,有可能会出现存一次,取两次等问题。这时就要了解到线程的等待与唤醒机制。相关方法有:

wait(): 告诉当前线程放弃执行权,并放弃监视器(锁)并进入阻塞状态,直到其它线程执行并持有了相同的锁并调用notify或notifyAll方法为止。
notify(): 唤醒使用一个锁中第一个调用wait()进入等待的线程,被唤醒的线程进入Runnable状态,等待cpu执行权。
notifyAll(): 唤醒使用同一监视器(锁)中调用wait的所有的线程。

存一次之后调用notify方法唤醒取一次,存一次调用wait进入等待。取一次拿到cpu执行权后,执行完毕调用notify唤醒取一次,取一次调用waiti进入等待。达到存一次取一次的效果。

小结:
1、线程间通信可以通过多个线程操作同一个资源进行数据交换,也可以通过锁对象进行线程之间交互,例如:锁对象.wait(),锁对象notify(),锁对象.notifyAll()都是对持有锁的线程的交互操作,一般使用在同步代码中,只有同步才具有锁。

2、一些方法定义在Object类中
等待和唤醒必须是同一个锁。而锁由于可以使用任何对象,故定义在Object类中。

3、wait() 和 sleep()有什么区别?
wait():释放资源,释放锁。是Object的方法
sleep():释放资源,不释放锁。是Thread的方法

4、定义了notify为什么还要定义notifyAll()
Notify:唤醒持有相关监视器(锁),并且第一个调用wait方法后处于等待状态的线程。
notifyAll:唤醒持有相关监视器(锁)的所有处于等待状态的其它线程。

结束线程

1.线程的run()执行完毕,线程自动结束。
2.线程的stop方法,但是会出现线程还没执行完毕,就结束了的问题,不建议使用。
3.用标记停止线程,给当前线程定义一个标记,通过另一个线程改变标记,达到停止当前线程的效果。

前台线程和后台线程

前台线程特点:
应用程序必须运行完所有前台线程才可退出,默认启动的线程是前台线程
后台(守护)线程:
隐藏起来默默运行的线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台(守护)线程在应用程序退出时都会自动结束。执行main方法的主线程就是一个前台线程,如果没有其它前台线程,main方法执行完成后程序将退出。

设置为后台线程:
setDaemon(boolean on)

使用注意:
必须在启动线程之前(调用start方法之前)调用setDaemon(true)方法,才可以把该线程设置为后台线程。

可以使用isDaemon() 测试该线程是否为后台线程(守护线程)。

Thread的join方法

Thread的join方法:Join可以用来临时加入线程执行,例如:A,B两个线程交替执行,当A线程执行到了B线程Join方法时,A就会等待,等B线程都执行完A才会执行。带参数的join方法可以指定合并时间,有纳秒和毫秒级别。

ThreadLocal类

用处:保存线程的独立变量。对一个线程类(继承自Thread),当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

实现:ThreadLocal都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是ThreadLocal放的是entry而不是entry的链表。功能还是一个map。)以线程对象本身为key,以目标为value。通过提供set(Object o)和get()方法来对map的数据进行操作。

jdk1.5后多线程锁

程序不止靠synchronized实现锁功能,jdk1.5后并发包中新增了java.util.concurrent.locks.Lock接口以及相关实现类来实现锁功能,更灵活、功能更强大、更面向对象。

lock接口方法:

void lock():获得锁。如果锁不可用,当前线程被禁用,进入休眠状态,直到获取锁。

void lockInterruptibly():获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断)。

Condition newCondition():获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。

boolean tryLock():只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。

boolean tryLock(long time, TimeUnit unit):超时获取锁,当前线程在以下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false.。

void unlock():释放锁。

Lock接口重要实现类:

ReentrantLock:排他锁

ReadWriteLock接口重要实现类:

ReentrantReadWriteLock:读写锁。
读写锁维护了两个锁,一个是读操作相关的锁也称为共享锁,一个是写操作相关的锁称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的。)。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

jdk1.5后多线程新的实现方式

实现Callable接口

1.实现Callable接口,重写接口的call方法。
2.FutureTask实例包装Callable实例,FutureTask实现了RunnableFuture接口,而Runnable接口又继承了Runnable和Future接口。这也就使得FutureTask能够作为Thread构造方法的一个参数,又能通过get方法获得返回值。
3.创建Thread实例包装FutureTask实例,调用Thread实例start方法启动线程。
4.通过FutureTask的get方法来获取返回值。

核心代码

public class Mycallable implements Calllable<String>{

		@Override
		public String call() throws Exception{
			//需要执行的代码块
		}

		main(){
			Callable<String> c=new Mycallable();		//创建实例对象
			FutureTask<String> f=new FutureTask<String>(c);		//创建FutureTask
			new Thread(f).start();					//启动线程,省略try-catch
			String result=f.get();			//获取返回结果
		}
}

使用线程池

1.使用Callable、ExecutorService和Future实现又返回结果的多线程。
2.Callable接口:无返回值的任务必须实现Runnable接口,有返回值的任务必须实现Callable接口,并重写call方法。
3.Future接口:用来获取Callable任务后结果,执行Callable任务后,可以获取一个Future的对象,在该对象是调用get方法就可以获取Callable的call方法的执行结果。
4.ExecutorService接口:通过Executors类获取线程池接口ExecutorService就可以实现有返回结果的多线程了。

核心代码

main(){
	ExecutorService pool=null;
		//定义连接数
		int poolSize=3;
		//定义线程任务数
		int taskSize=5;
		//创建一个线程池,定制线程数目为poolSize
		pool=Executors.newFixedThreadPool(poolSize);
		//创建Callable对象
		Callable<String> c=new Mycallable();	//Mycallable为Callable实现类
		//五个任务三个线程
		for(int i=0;i<taskSize;i++){
		//获取结果
			Future<String> f=pool.submit(c);
			sysout(f.get());
			
			//线程池关闭
			pool.shutdown();
			}
}

猜你喜欢

转载自blog.csdn.net/weixin_44158992/article/details/108621322