设为首页 加入收藏

TOP

Java 线程和多线程执行过程分析(一)
2018-01-17 13:05:45 】 浏览:531
Tags:Java 线程 执行 过程 分析

本文目录:
1.几个基本的概念
2.创建线程的两种方法
3.线程相关的常用方法
4.多线程安全问题和线程同步
 4.1 多线程安全问题
 4.2 线程同步
 4.3 同步代码块和同步函数的区别以及锁是什么
 4.4 单例懒汉模式的多线程安全问题
5.死锁(DeadLock)


 


本文涉及到的一些概念,有些是基础知识,有些在后文会展开详细的说明。


还需需要明确的一个关键点是:CPU对就绪队列中每个线程的调度是随机的(对我们人类来说),且分配的时间片也是随机的(对人类来说)。


 


Java中有两种创建线程的方式。


创建线程方式一:


例如下面的代码中,在主线程main中创建了两个线程对象,先后并先后调用start()开启这两个线程,这两个线程会各自执行MyThread中的run()方法。


上面的代码执行时,有三个线程,首先是主线程main创建2个线程对象,并开启这两个线程任务,开启两个线程后主线程输出"main thread over",然后main线程结束。在开启两个线程任务后,这两个线程加入到了就绪队列等待CPU的调度执行。如下图。因为每个线程被cpu调度是随机的,执行时间也是随机的,所以即使mt1先开启任务,但mt2可能会比mt1线程先执行,也可能更先消亡。



创建线程方式二:


这两种创建线程的方法,无疑第二种(实现Runnable接口)要好一些,因为第一种创建方法继承了Thread后就无法继承其他父类。


 


Thread类中的方法:


Object类中的方法:


这里的某个线程池是由锁对象决定的。持有相同锁对象的线程属于同一个线程池。见后文。


一般来说,wait()和唤醒的notify()或notifyAll()是成对出现的,否则很容易出现死锁。


sleep()和wait()的区别:(1)所属类不同:sleep()在Thread类中,wait()则是在Object中;(2)sleep()可以指定睡眠时间,wait()虽然也可以指定睡眠时间,但大多数时候都不会去指定;(3)sleep()不会抛异常,而wait()会抛异常;(4)sleep()可以在任何地方使用,而wait()必须在同步代码块或同步函数中使用;(5)最大的区别是sleep()睡眠时不会释放锁,不会进入特定的线程池,在睡眠时间结束后自动苏醒并继续往下执行任务,而wait()睡眠时会释放锁,进入线程池,等待notify()或notifyAll()的唤醒。


java.util.concurrent.locks包中的类和它们的方法:


 


 


线程安全问题是指多线程同时执行时,对同一资源的并发操作会导致资源数据的混乱。


例如下面是用多个线程(窗口)售票的代码。


执行结果大致如下:



以上代码的执行过程大致如下图:



共开启了4个线程执行任务(不考虑main主线程),每一个线程都有4个任务:


这四个任务的共同点也是关键点在于它们都操作同一个资源Ticket对象中的num,这是多线程出现安全问题的本质,也是分析多线程执行过程的切入点


当main线程开启t1-t4这4个线程时,它们首先进入就绪队列等待被CPU随机选中。(1).假如t1被先选中,分配的时间片执行到任务②就结束了,于是t1进入就绪队列等待被CPU随机选中,此时票数num自减后为99;(2).当t3被CPU选中时,t3所读取到的num也为99,假如t3分配到的时间片在执行到任务②也结束了,此时票数num自减后为98;(3).同理t2被选中执行到任务②结束后,num为97;(4).此时t3又被选中了,于是可以执行任务③,甚至是任务④,假设执行完任务④时间片才结束,于是t3的打印语句打印出来的num结果为97;(5).t1又被选中了,于是任务④打印出来的num也为97。


显然,上面的代码有几个问题:(1)有些票没有卖出去了但是没有记录;(2)有的票重复卖了。这就是线程安全问题。


 


java中解决线程安全问题的方法是使用互斥锁,也可称之为"同步"。解决思路如下:


(1).为待执行的任务设定给定一把锁,拥有相同锁对象的线程在wait()时会进入同一个线程池睡眠。
(2).线程在执行这个设了锁的任务时,首先判断锁是否空闲(即锁处于释放状态),如果空闲则去持有这把锁,只有持有这把锁的线程才能执行这个任务。即使时间片到了,它也不是释放锁,只有wait()或线程结束时才会安全地释放锁。
(3).这样一来,锁被某个线程持有时,其他线程在锁判断后就继续会线程池睡眠去了(或就绪队列)。最终导致的结果是,(设计合理的情况下)某个线程一定完整地执行完一个任务,其他线程才有机会去持有锁并执行任务。


换句话说,使用同步线程,可以保证线程执行的任务具有原子性,只要某个同步任务开始执行了就一定执行结束,且不允许其他线程参与。


让线程同步的方式有两种,一种是使用synchronized(){}代码块,一种是使用synchronized关键字修饰待保证同步的方法。


使用同步之后,if(num>0)num--return numprint(num)这4个任务就强制具有原子性。某个线程只要开始执行了if语句,它就一定会继续执行直到执行完print(num),才算完成了一整个任务。只有完成了一整个任务,线程才会释放锁(当然,也可能继续判断while(true)并进入下一个循环)。


 


前面的示例中,同步代码块synchronized(obj){}中传递了一个obj的Object对象,这个obj可以是任意一个对象的引用,这些引用传递给代码块的作用是为了标识这个同步任务所属的锁。


synchronized函数的本质其实是使用了this作为这个同步函数的锁标识,this代表的是当前对象的引用。但如果同步函数是静态的,即使用了static修饰,则此时this还没出现,它使用的锁是"类名.class"这个字节码文件对象,对于java来说,这也是一个对象,而且一个类中一定有这个对象。


使用相同的锁之间会互斥,但不同锁之间则没有任何影响。因此,要保证任务同步(原子性),这些任务所关联的锁必须相同。也因此,如果有多个同步任务(各自保证自己的同步性),就一定不能都使用同步函数。


例如下面的例子中,写了两个相同的sale()方法,并且使用了flag标记让不同线程能执行这两个同步任务。如果出现了多线程安全问题,则表明synchronized函数和同步代码块使用的是不同对象锁。如果将同步代码块中的对象改为this后不出现多线程安全问题,则表明同步函数使用的是this对象。如果为sale2()加上静态修饰static,则将obj替换为"Ticket.class"来测试。


以下是执行结果中的一小片段,出现了多线程安全问题。而如果将同步代码块中的obj改为this,则不会出现多线程安全问题。


 


单例饿汉式:


单例懒汉式:


当多线程操作单例饿汉式和懒汉式对象的资源时,是否有多线程安全问题?


以上面的代码为例。当多线程分别被CPU调度时,饿汉式中的getInstance()返回的s,s是final属性修饰的,因此随便哪个线程访问都是固定不变的。而懒汉式则随着不同线程的来临,不断new Single(),也就是说各个线程

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇JVM类加载机制以及类缓存问题的处.. 下一篇JavaScript中比较运算符的使用

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目