多线程
程序和进程的概念------------------------------
程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件
进程 - 主要指运行在内存中的可执行文件
目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务
但进程是重量级的,也就是新建一个进程会消耗CPU和内存空间等系统资源,因此进程的数量比较局限
通常一个进程包含一个或者多个线程,而一个进程可能占用一个或者多个端口
但一个端口基本只能由一个进程占用(如果以后说明一个进程基本占用一个端口,那么就是这个意思,虽然可以占用多个)
要注意:只有与网络有关的进程才需要占用端口号,因为端口只是操作网络的而已,对于内部的其他操作,进程并非一定操作端口哦,反正是线程组(当然,进程也包含其他东西,如资源分配操作(因为我们可以继续创建线程),所以也不只是线程组,这也是为什么我们大多会称为进程,而不是线程组的原因)
线程的概念------------------------------
为了解决上述问题就提出线程的概念,线程就是进程内部的程序流
也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的
线程是轻量的,新建线程会共享所在进程的系统资源(如CPU和内存空间),也就是说一个进程的总资源是有限的,一般是该进程获取的(具体如何获取可以百度,这不是java要做的事情),因此目前主流的开发都是采用多线程
多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制(单核来说)
对于单核CUP来说:多个线程就是看起来是一起运行的,但是实际上没有一起运行
因为运行速度太快,看不出来(仔细一看不是一起运行的,粗略一看是一起运行的)
对于多核CUP来说(一个cpu可以有多个核心,各个核心可以操作线程,也可以存在多个cpu):多个线程若分配与两个或者两个以上的CPU同时运行,一般核心也算,那么就有可能真正的同时运行
如两个线程分别由各自的CPU来操作,只是占资源,操作由CPU操作,一般我们以多核为主,因为现在的电脑基本上都是多核
对于进程来说,我们通常在进程里运行多个线程,且线程占用的CPU和内存空间资源都是由进程的CPU和内存空间资源分配的
而进程的CPU和内存资源都是由系统资源分配,如一些电脑的内存空间大小有8G或者16G内存
而CPU资源也要看型号或者主频,外频,总线频率
线程的创建------------------------------
public class Thread
extends Object
implements Runnable
java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例
Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性
创建方式------------------------------
自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法
自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象
然后使用Thread类型的对象调用start方法
相关的方法------------------------------
Thread ( ) ,使用无参的方式构造对象
Thread ( String name) ,根据参数指定的名称来构造对象
Thread ( Runnable target) ,根据参数指定的引用来构造对象,其中Runnable 是个接口类型
Thread ( Runnable target, String name) ,根据参数指定引用和名称来构造对象
void run ( ) ,若使用Runnable 引用构造了线程对象,调用该方法时最终调用接口中的版本
若没有使用Runnable 引用构造线程对象,调用该方法时则啥也不做
void start ( ) ,用于启动线程,Java 虚拟机会自动调用该线程的run方法
package com. lagou. task18 ;
public class ThreadTest {
public static void main ( String [ ] args) {
Thread t1 = new Thread ( ) ;
Thread t2 = new Thread ( "1" ) ;
t1. run ( ) ;
System . out. println ( "我想看看你到底是否真的啥也不干!" ) ;
}
}
执行流程------------------------------
执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程
main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成了2个
新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响,当run方法执行完毕后子线程结束
当main方法执行完毕后主线程结束,一般两个线程(一般不包括主线程)执行没有明确的先后执行次序,由操作系统调度算法 来决定
可以看作谁先用CPU执行 ,即也可以认为是随机的,这里之所以不包括主线程,因为主线程必然是先占用cpu的,就相当于在线程里进行操作创建线程,而不是同时进行操作,所以主线程基本会优先
即对于单核来说:虽然在main里面创建的线程,但还会与main主线程抢占CPU资源,即谁先抢占(操作系统调度算法 ),谁先执行
不会因为该线程是主线程的子线程而不进行抢夺
如:它先分时间片给第一个调用,然后时间到了,把第一个踢出去
再把时间片分给第二个调用,时间到了又把第二个踢出去
又让第一个来,就好像2个人排队玩游戏,一人5分钟
即时间片是先分配好的,然后再继续分配,通常叫做抢占,即它通常会依次运行,若你运行时,程序有输出多条的
基本上是多核或者程序运行时间的差异,最有可能是优先级的关系(优先级大分配的时间多,即一般总体可以运行的时间久)
若还是会有一个输出两次的,则应该是内部运行时间的差异
那么就有可能会依次运行(程序时间不相差过大,而多核在相差不多时,会出现不同结果,而单核不会,分配到同一个CPU里面,微观上是依次执行)
之所以是有可能,是因为程序运行时间的不同,而导致某个程序比其他程序多运行几次,然后他才运行完,但时间是一样的
但对于多核来说:就会同时运行了,但一开始还是会先占CPU的先运行(注意:一般不是抢占对方的,极少数情况下,与单核一样,抢占同一个,或者所有的CPU都有线程了,那么在对应同一个CPU下,可以认为是单核了,该cpu可以指核心或者不同cpu),只不过之后不会再依次运行,因为由不同CPU操作了(不同的核,可以称为不同的CPU),也就是说,多核情况下,一个线程占用一个核心,如果核心够多,其他核心是没有被使用的
package com. lagou. task18 ;
public class SubThreadRun extends Thread {
@Override
public void run ( ) {
for ( int i = 1 ; i <= 20 ; i++ ) {
System . out. println ( "run方法中:i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class SubThreadRunTest {
public static void main ( String [ ] args) {
Thread t1 = new SubThreadRun ( ) ;
t1. start ( ) ;
for ( int i = 1 ; i <= 20 ; i++ ) {
System . out. println ( "-----------------main方法中:i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class SubRunnableRun implements Runnable {
@Override
public void run ( ) {
for ( int i = 1 ; i <= 20 ; i++ ) {
System . out. println ( "run方法中:i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class SubRunnableRunTest {
public static void main ( String [ ] args) {
SubRunnableRun srr = new SubRunnableRun ( ) ;
Thread t1 = new Thread ( srr) ;
t1. start ( ) ;
for ( int i = 1 ; i <= 20 ; i++ ) {
System . out. println ( "-----------------main方法中:i = " + i) ;
}
}
}
方式的比较------------------------------
继承Thread类的方式代码简单,但是若该类继承Thread类后则无法继承其它类
而实现Runnable接口的方式代码复杂,但不影响该类继承其它类以及实现其它接口,因此以后的开发中推荐使用第二种方式
匿名内部类的方式------------------------------
使用匿名内部类的方式来创建和启动线程
package com. lagou. task18 ;
public class ThreadNoNameTest {
public static void main ( String [ ] args) {
new Thread ( ) {
@Override
public void run ( ) {
System . out. println ( "张三说:在吗?" ) ;
}
} . start ( ) ;
Runnable ra = new Runnable ( ) {
@Override
public void run ( ) {
System . out. println ( "李四说:不在。" ) ;
}
} ;
Thread t2 = new Thread ( ra) ;
t2. start ( ) ;
new Thread ( ( ) -> System . out. println ( "李四说:不在。" ) ) . start ( ) ;
}
}
线程的生命周期------------------------------
新建状态 - 使用new关键字创建之后进入的状态,此时线程并没有开始执行
就绪状态 - 调用start方法后进入的状态,此时线程还是没有开始执行(或者说还没有准备执行run方法)
运行状态 - 使用线程调度器调用该线程后进入的状态,此时线程开始执行
当线程的时间片执行完毕后,任务没有完成时,回到就绪状态((可以认为是两个线程或者多个线程互相抢夺时间片,注意这个时间片只是一开始的,即与开始的线程进行抢夺,之后就不考虑时间片了,除非再次的抢)
消亡状态 - 当线程的任务执行完成后进入的状态,此时线程已经终止
阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法,阻塞状态解除后进入就绪状态
也就是说:
新建状态:NEW(在Thread里面的,State枚举里面可以知道)
就绪状态和运行状态:RUNNABLE
阻塞状态:BLOCKED,WAITING,TIME_WAITING
消亡状态:TERMINATED
即不管怎样,无论你是阻塞还是没有在运行完,最后都要重新在运行的地方开始,即就绪状态,但并没有清除你以前的行为
且创建时,只要是关于Thread类的,即调用了他的构造方法,基本上就是创建一个线程
注意:若直接输出直接量,且放在开启线程(start方法)后面,基本上就会先运行该输出的值,因为你在运行run方法时
需要输出的时间远远大于直接输出直接量,所以通常会发现你run在运行时,还没输出数值时,他就输出了(多核来说,单核也会)
实际上是他快
但对于单核,则有可能会依次运行(程序时间不相差过大,而多核在相差不多时,会出现不同结果,而单核不会,分配到同一个CPU里面,微观上是依次执行)
之所以是有可能,是因为程序运行时间的不同,而导致某个程序比其他程序多运行几次,然后他才运行完,但时间是一样的
线程的编号和名称------------------------------
long getId ( ) ,获取调用对象所表示线程的编号
String getName ( ) ,获取调用对象所表示线程的名称
void setName ( String name) ,设置/ 修改线程的名称为参数指定的数值
static Thread currentThread ( ) ,获取当前正在执行线程的引用
案例题目
自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的编号和名称
然后将线程的名称修改为"zhangfei"后再次打印编号和名称。要求在main方法中也要打印主线程的编号和名称
package com. lagou. task18 ;
public class ThreadIdNameTest extends Thread {
public ThreadIdNameTest ( String name) {
super ( name) ;
}
@Override
public void run ( ) {
System . out. println ( "子线程的编号是:" + getId ( ) + ",名称是:" + getName ( ) ) ;
setName ( "zhangfei" ) ;
System . out. println ( "修改后子线程的编号是:" + getId ( ) + ",名称是:" + getName ( ) ) ;
}
public static void main ( String [ ] args) {
ThreadIdNameTest tint = new ThreadIdNameTest ( "guanyu" ) ;
tint. start ( ) ;
Thread t1 = Thread . currentThread ( ) ;
System . out. println ( "主线程的编号是:" + t1. getId ( ) + ", 名称是:" + t1. getName ( ) ) ;
System . out. println ( System . currentTimeMillis ( ) ) ;
}
}
package com. lagou. task18 ;
public class RunnableIdNameTest implements Runnable {
@Override
public void run ( ) {
Thread t1 = Thread . currentThread ( ) ;
System . out. println ( "子线程的编号是:" + t1. getId ( ) + ", 名称是:" + t1. getName ( ) ) ;
t1. setName ( "zhangfei" ) ;
System . out. println ( "修改后子线程的编号是:" + t1. getId ( ) + ", 名称是:" + t1. getName ( ) ) ;
}
public static void main ( String [ ] args) {
RunnableIdNameTest rint = new RunnableIdNameTest ( ) ;
Thread t2 = new Thread ( rint, "guanyu" ) ;
t2. start ( ) ;
Thread t1 = Thread . currentThread ( ) ;
System . out. println ( "主线程的编号是:" + t1. getId ( ) + ", 名称是:" + t1. getName ( ) ) ;
}
}
常用的方法------------------------------
static void yield ( ) ,当前线程让出处理器(离开Running 状态),使当前线程进入Runnable 状态等待,不占用CPU 资源(相当于我直接退出重新抢夺时间片,但是由于他并不是重新调度,所以还是同一个cpu,即占用资源,但是随着时间发展,可能也会在某个时候改变cpu核心,只是可能性很少),但在对应的地方直接等待了(下次到对应等待的yield 后面执行)
static void sleep ( times) ,使当前线程从 Running (Runnable ,这是一个状态,要注意哦) 放弃处理器进入Block 状态,休眠times毫秒
再返回到Runnable 如果其他线程打断当前线程的Block ( sleep) (还是使用当前线程的引用,比如可以考虑静态的), 就会发生InterruptedException
上面对状态的说明,了解即可,具体可以查看101 章博客
因为有阻塞状态,即休眠的毫秒数,所以(间接,实际上是没有的)占用CPU 资源(解释:指的是监视,而非sleep自身占用(对与自身,在阻塞时自然没有占用cpu资源,也就会释放对应资源,使得cpu资源回收,大多数博客说明的都是这个),一般监视是其他某些线程考虑的,所以这也是为什么大多数博客说他没有占用的原因,但也正是因为这样才会一直占用,而wait什么的是等待唤醒,没有什么一直操作的执行,因为是手动的,所以他完全不占用),因为休眠不结束的话,就会一直运行
但会有他里面代码的内存占用,但占用内存是必然的,其他程序也都需要内存,所以这里不用考虑内存
但是也要注意:休眠本身也是占用资源的,sleep虽然是休眠,他只是不消耗cpu资源,但是他占用者线程资源(线程没有释放),cpu是描述动的时候的资源,你不操作,就不占用,也就是说消耗cpu等价于占用,除了cpu还有内存等等资源(线程资源属于内存,其他资源基本也都是属于内存,虽然也存在不是内存的一些文件访问,网络连接的其他资源,是否可以访问也是资源,任何东西都可以是资源)
int getPriority ( ) ,获取线程的优先级
void setPriority ( int newPriority) ,修改线程的优先级
优先级越高的线程不一定先执行,但该线程获取到时间片的机会会更多一些( 一般总体时间更长)
除了程序也有运行时间,过大,则会后出结果
最主要的就是设置的优先级,不会真正的优先,就如概率一样,99 % 正确也会有1 % 的错误,可以将优先级看成概率
优先级默认为5 ,当然,优先级是看相对的,如果你的优先级是99 ,他也是99 ,那么就与5 和5 是一样的,所以是相对的,即我占二分之1 ,即50 % 概率,一般来说他们概率都相同(5 ),所以抢占基本随机
注意:线程优先级的影响对多核核心的情况是减弱的,因为核心充沛的情况下,即当处理器核心数量充足且负载较轻时,线程优先级的直接影响可能确实减少,因为大部分或所有线程都可以同时运行,没有必要竞争处理器资源,在这种情况下,每个线程都有可能获得一个核心,从而减少了优先级对实际运行性能的影响,然而,即使在核心充足的环境中,优先级仍然有其潜在的作用,因为其他系统资源(如内存、I / O 操作等)可能仍然存在竞争,并且资源并不是一直充沛的,所以线程优先级的设置还是有一定的作用,并且即使核心充沛,优先级可能也会有作用,比如谁先执行,而不是同时处理了
void join ( ) ,等待该线程终止,即主线程会等待,但是主线程里的其他子线程有启动的,那么那个子线程会继续运行
而不会等待
void join ( long millis) ,等待参数指定的毫秒数
boolean isDaemon ( ) ,用于判断是否为守护线程
void setDaemon ( boolean on) ,用于设置线程为守护线程
其他程序运行时间,也会导致sleep的操作
如我要一个数值输出8次,每停顿一秒输出一次,而主线程停顿8秒,打印主线程等待结束,通常情况下,都会输出8次
但是我在第一次的时候,就写一个很大的循环,可以循环8秒的循环,那么就会出现,只输出一次的结果
即可以对于一般程序来说,基本不会影响,因为他所占的时间对于一秒来说可以忽略不计
如主线程停5秒,子线程停1秒,主线程的5秒会导致子线程的判断条件为false,如下
white ( a == true ) {
System . out. println ( 1 ) ;
Thread . sleep ( 1000 ) ;
}
Thread . sleep ( 5000 ) ;
a = false ;
1 -- -- -- -- 1 -- -- -- -- 1 -- -- -- -- 1 -- -- -- -- 1 -- -- -- -- a == ture ( 不成立)
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- a = false
white ( a == true ) {
Thread . sleep ( 1000 ) ;
System . out. println ( 1 ) ;
}
Thread . sleep ( 5000 ) ;
a = false ;
-- -- -- -- 1 -- -- -- -- 1 -- -- -- -- 1 -- -- -- -- 1 -- -- -- -- 1 a == ture ( 不成立)
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- a = false
package com. lagou. task18 ;
public class ThreadPriorityTest extends Thread {
@Override
public void run ( ) {
for ( int i = 0 ; i < 20 ; i++ ) {
System . out. println ( "子线程中:i = " + i) ;
}
}
public static void main ( String [ ] args) {
ThreadPriorityTest tpt = new ThreadPriorityTest ( ) ;
tpt. setPriority ( Thread . MAX_PRIORITY ) ;
tpt. start ( ) ;
Thread t1 = Thread . currentThread ( ) ;
for ( int i = 0 ; i < 20 ; i++ ) {
System . out. println ( "--主线程中:i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class ThreadJoinTest extends Thread {
@Override
public void run ( ) {
System . out. println ( "倒计时开始..." ) ;
for ( int i = 10 ; i > 0 ; i-- ) {
System . out. println ( i) ;
try {
Thread . sleep ( 1000 ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
}
System . out. println ( "新年快乐!" ) ;
}
public static void main ( String [ ] args) {
ThreadJoinTest tjt = new ThreadJoinTest ( ) ;
tjt. start ( ) ;
System . out. println ( "主线程开始等待..." ) ;
try {
tjt. join ( 5000 ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "可惜不是你,陪我到最后!" ) ;
}
}
package com. lagou. task18 ;
public class ThreadDaemonTest extends Thread {
@Override
public void run ( ) {
for ( int i = 0 ; i < 50 ; i++ ) {
System . out. println ( "子线程中:i = " + i) ;
}
}
public static void main ( String [ ] args) {
ThreadDaemonTest tdt = new ThreadDaemonTest ( ) ;
tdt. setDaemon ( true ) ;
tdt. start ( ) ;
for ( int i = 0 ; i < 20 ; i++ ) {
System . out. println ( "-------主线程中:i = " + i) ;
}
}
}
主线程:是产生其他子线程的线程,通常它必须最后完成执行比如执行各种关闭动作
即可以说守护线程是给主线程操作子线程的一种设置
案例题目------------------------------
编程创建两个线程,线程一负责打印1 ~ 100之间的所有奇数,其中线程二负责打印1 ~ 100之间的所有偶数
在main方法启动上述两个线程同时执行,主线程等待两个线程终止
package com. lagou. task18 ;
public class SubThread1 extends Thread {
@Override
public void run ( ) {
for ( int i = 1 ; i <= 100 ; i += 2 ) {
System . out. println ( "子线程一中: i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class SubThread2 extends Thread {
@Override
public void run ( ) {
for ( int i = 2 ; i <= 100 ; i += 2 ) {
System . out. println ( "------子线程二中: i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class SubThreadTest {
public static void main ( String [ ] args) {
SubThread1 st1 = new SubThread1 ( ) ;
SubThread2 st2 = new SubThread2 ( ) ;
st1. start ( ) ;
st2. start ( ) ;
System . out. println ( "主线程开始等待..." ) ;
try {
st1. join ( ) ;
st2. join ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "主线程等待结束!" ) ;
}
}
package com. lagou. task18 ;
public class SubRunnable1 implements Runnable {
@Override
public void run ( ) {
for ( int i = 1 ; i <= 100 ; i += 2 ) {
System . out. println ( "子线程一中: i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class SubRunnable2 implements Runnable {
@Override
public void run ( ) {
for ( int i = 2 ; i <= 100 ; i += 2 ) {
System . out. println ( "------子线程二中: i = " + i) ;
}
}
}
package com. lagou. task18 ;
public class SubRunnableTest {
public static void main ( String [ ] args) {
SubRunnable1 sr1 = new SubRunnable1 ( ) ;
SubRunnable2 sr2 = new SubRunnable2 ( ) ;
Thread t1 = new Thread ( sr1) ;
Thread t2 = new Thread ( sr2) ;
t1. start ( ) ;
t2. start ( ) ;
System . out. println ( "主线程开始等待..." ) ;
try {
t1. join ( ) ;
t2. join ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "主线程等待结束!" ) ;
}
}
线程同步机制------------------------------
当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题
即当多个线程需要访问同一个资源时
它们需要以某种顺序来确保该资源在某时刻只能被一个线程使用,否则,程序的运行结果将会是不可预料的
此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制
多个线程并发读写同一个临界资源时会发生线程并发安全问题
异步操作:多线程并发(一起进发)的操作,各自独立运行,可以理解位不同的步骤,即不用排队
同步操作:多线程串行(跟着进行)的操作,先后执行的顺序,可以理解为同样的步骤,即排队
对于同步机制,主要针对于多核,因为单核实际上还是依次运行的
对于多个start方法,先调用的一般先执行,只不过现在的电脑处理很快的,你几乎都感觉不到在并发
但也只是先执行的速度快而已,CPU先运算的还是会先执行
解决方案------------------------------
先看后面的代码:
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理
引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率
实现方式------------------------------
在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性
即要么我不执行,要么等我执行完,且执行过程不可打断,若有多个在等待,那么他们自己之间进行抢夺资源
来保证谁先进入,即也相当于操作系统调度算法 来决定
具体方式如下:
使用同步代码块的方式实现部分代码的锁定,格式如下:
synchronized ( 类类型的引用) {
编写所有需要锁定的代码
}
使用同步方法的方式实现所有代码的锁定,直接使用synchronized 关键字来修饰整个方法即可该方式等价于:
synchronized ( this ) {
整个方法体的代码
}
即直接用synchronized 对方法修饰时,就说明这个方法是谁调用谁来当同步监听器,即等价于this
换言之,就是这个方法被锁了,只对于同一引用,因为其他引用调用的this 是不同的
即这就是为什么需要一个同步监听器的原因
由于synchronized ( 类类型的引用) 的"类类型的引用" 是多变的,即不同的引用也会参与同步,如静态的引用
但基本上都会在类里面新建该引用,防止同一引用的调用,会出现不同步,如synchronized ( new 类名( ) )
但是正是因为有可能会没有新建,则出现了直接修饰整个方法,即相当于this ,这样就保证了是同一引用的会同步(认为是对该引用加上锁的相关标识,在以后或说明的,比如101 章博客中的state状态的线程标识位)
而对于静态来说,由于没有this ,但是静态的基本上都是一个类的,即使用了该静态,就一定是使用了同一个类的方法
那么这时使用synchronized 修饰方法时等价于使用了synchronized ( 类名. class ) (通常是当前类,即静态方法所在的类),这个类名无论怎么变化,他们在调用这个静态方法时都会同步( 串行)
最后可以理解为无论是什么方法或者代码,在有synchronized 锁面前,只要( ) 里面的同步监听器是相同的
相同监听器所修饰的锁都必须等待前一个的锁打开( 运行完,而Lock 必须解锁)
否则,无论你是不同的方法,还是不同的类的方法,都必须等一等
回忆:继承类时相同方法都有重写,一个类的方法是在方法区里,且独一份,并且类里面最好不要创建本类的对象
因为有嵌套,如main方法里创建对象时,运行时有问题,或者静态对象调用时,也会出现问题
当然静态变量在本类方法里,可以直接写出,或者加上类名写出都可以
package com. lagou. task18 ;
public class AccountThreadTest extends Thread {
private int balance;
private static Demo dm = new Demo ( ) ;
public AccountThreadTest ( ) {
}
public AccountThreadTest ( int balance) {
this . balance = balance;
}
public int getBalance ( ) {
return balance;
}
public void setBalance ( int balance) {
this . balance = balance;
}
@Override
public void run ( ) {
test ( ) ;
}
public static void test ( ) {
synchronized ( AccountThreadTest . class ) {
System . out. println ( "线程" + Thread . currentThread ( ) . getName ( ) + "已启动..." ) ;
int temp = 1000 ;
if ( temp >= 200 ) {
System . out. println ( "正在出钞,请稍后..." ) ;
temp -= 200 ;
try {
Thread . sleep ( 5000 ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "请取走您的钞票!" ) ;
} else {
System . out. println ( "余额不足,请核对您的账户余额!" ) ;
}
}
}
public static void main ( String [ ] args) {
AccountThreadTest att1 = new AccountThreadTest ( 1000 ) ;
att1. start ( ) ;
AccountThreadTest att2 = new AccountThreadTest ( 1000 ) ;
att2. start ( ) ;
System . out. println ( "主线程开始等待..." ) ;
try {
att1. join ( ) ;
att2. join ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "最终的账户余额为:" + att1. getBalance ( ) ) ;
}
}
package com. lagou. task18 ;
import java. util. concurrent. locks. ReentrantLock ;
public class AccountRunnableTest implements Runnable {
private int balance;
private Demo dm = new Demo ( ) ;
private ReentrantLock lock = new ReentrantLock ( ) ;
public AccountRunnableTest ( ) {
}
public AccountRunnableTest ( int balance) {
this . balance = balance;
}
public int getBalance ( ) {
return balance;
}
public void setBalance ( int balance) {
this . balance = balance;
}
@Override
public void run ( ) {
lock. lock ( ) ;
System . out. println ( "线程" + Thread . currentThread ( ) . getName ( ) + "已启动..." ) ;
int temp = getBalance ( ) ;
if ( temp >= 200 ) {
System . out. println ( "正在出钞,请稍后..." ) ;
temp -= 200 ;
try {
Thread . sleep ( 5000 ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "请取走您的钞票!" ) ;
} else {
System . out. println ( "余额不足,请核对您的账户余额!" ) ;
}
setBalance ( temp) ;
lock. unlock ( ) ;
}
public static void main ( String [ ] args) {
AccountRunnableTest account = new AccountRunnableTest ( 1000 ) ;
Thread t1 = new Thread ( account) ;
Thread t2 = new Thread ( account) ;
t1. start ( ) ;
t2. start ( ) ;
System . out. println ( "主线程开始等待..." ) ;
try {
t1. join ( ) ;
t2. join ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "最终的账户余额为:" + account. getBalance ( ) ) ;
}
}
class Demo { }
注意:一个类里面可以写多个类(不是内部类),而文件名通常必须要与有public修饰的类名一样,否则编译时会报错
之所以是通常,是因为若都没有public那么就会编译通过,会出现多个类的字节码文件
即使用java xxx(那些字节码文件的名字,不修改的话即类名),会使用相应的类的main方法
但是若直接用java xxx.java则会执行,他跳过了该错误
而他没有直接的看到出现的字节码文件,那么在运行时,会根据类的执行顺序而运行main方法
且不管你是否为public修饰的类,即只运行放在前面的类的main方法
静态方法的锁定------------------------------
当我们对一个静态方法加锁,如:
public synchronized static void xxx ( ) { …. }
那么该方法锁的对象是类对象
每个类都有唯一的一个类对象
获取类对象的方式: 类名. class
静态方法与非静态方法同时使用了synchronized 后它们之间是非互斥关系的
原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象
静态变量是有引用的连接的,即也可以用引用来调用
所有的修饰都基本上写在返回值前面,而返回值前面的修饰基本上是可以改变位置的
注意事项------------------------------
使用synchronized保证线程同步应当注意:多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用
即当使用类实现接口时,由于Thread都使用该类,即调用该类的run方法,最后创建多个Thread时
调用的都是同一个对象的run方法
即synchronized里的引用若使用本类创建的引用时,使用的是同一个
若使用类继承Thread的话,那么调用的是不同的对象的run方法,那么synchronized里的引用若使用本类创建的引用时
使用的不是同一个
在使用同步块时应当尽量减少同步范围以提高并发的执行效率
线程安全类和不安全类------------------------------
StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类
Vector类和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类,一般我们都会考虑使用不安全的,因为效率高
Collections.synchronizedList() 和 Collections.synchronizedMap()等的返回结果对象中,方法基本都是安全的
死锁的概念------------------------------
线程一执行的代码:
public void run ( ) {
synchronized ( a) {
synchronized ( b) {
编写锁定的代码;
}
}
}
线程二执行的代码:
public void run ( ) {
synchronized ( b) {
synchronized ( a) {
编写锁定的代码;
}
}
}
注意:在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用
使用Lock(锁)实现线程同步------------------------------
从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具
该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性
在以后的线程安全控制中,经常使用ReentrantLock类显式加锁和释放锁
常用的方法------------------------------
ReentrantLock ( ) ,使用无参方式构造对象
void lock ( ) ,获取锁
void unlock ( ) ,释放锁
具体代码在前面操作过了
与synchronized方式的比较------------------------------
Lock是显式锁,需要手动实现开启和关闭操作
而synchronized是隐式锁,执行锁定代码后自动释放
Lock只有同步代码块方式的锁,即锁与解锁之间的代码
与synchronized不一样,synchronized是运行完后,才可让其他代码进入,而Lock必须要解锁后才可进入
而synchronized有同步代码块方式和同步方法两种锁
使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好
由于Lock可以实现手动操作,即可以有更多的操作空间
Object类常用的方法------------------------------
void wait ( ) ,用于使得线程进入等待状态( 也是阻塞状态,类似于sleep方法,他们都是阻塞状态,只是阻塞的方式不同而已) ,直到其它线程调用notify ( ) 或notifyAll ( ) 方法
且占用CPU 资源( 针对判断他的位置来说的,否则是没有占用的,就如sleep的监视一样,并且他的唤醒是重新调度,所以可能回到其他cpu核心,在同一个cpu核心上,就代表执行权没有改变,一般的,这都是因为算法来判断的,而重新调度,自然是多核心情况下,所以他都是有可能的,而sleep一般不会这样) ,并会解除对象锁,而sleep不会
wait ( ) 方法会强迫线程先进行释放锁操作,即必须要在锁里面进行,否则报错
即可以知道,他可以随时释放锁,与unlock方法类似( 只是类似,因为他释放的是synchronized 锁)
但他也可以让该线程进行等待(对应地方等待,只是其他的线程可以进入,但他还是在哪个地方,并没有离开,即没有结束执行),且必须唤醒或者等待他时间结束(后面的操作方法),这里是必要的
所以也可以看成sleep与unlock的结合体,即可以有更多的操作空间
如互相输出,即线程的来回操作
也可以让两个线程同时操作,如当唤醒后或者等待时间结束后,一个操作wait方法前面的,另一个操作wait方法后面的
但必须要在synchronized 锁里面,Lock 锁里面可能也不行(实际上也可以,具体可以百度,一般不能直接的操作,需要在里面又加上synchronized ,特别的,若要直接实现对应的操作,一般需要Condition ,一般是接口,通常使用ReentrantLock 来进行创建对象),因为底层代码的缘故,wait ( ) 方法会强迫线程先进行释放锁操作
即必须要在synchronized 锁里面进行,否则报错,因为wait就是用来操作锁的(包括与他相关的解除,如notify),即在锁里面调用wait方法时
可以隐式的看成监听器. wait ( ) ,所以当syschronized的参数为this 时,可以直接写wait ( ) ,否则需要加参数名. wait ( ) (当然,如果对应的参数是直接new 的,那么基本上不能显示调用了),即如果有参数,那么必须参数来调用(比如Object a = new Object ( ) ; ,syschronized参数设置为a,那么就要a. wait ( ) ,否则直接的wait ( ) 会报错),而new 基本不能调用,否则也报错
除非他有隐式的this ,如引用调用时,就有this ,要不然就报错
即虽然是服务与锁,但实际上服务于监听器,即锁可以称为监听器的锁
那么就会有,当调用方法时,若引用相同( 设为引用为c) ,且该方法里的synchronized 的监听器为this
那么该锁里面写上wait或者notify,也就是c. wait或者c. notify,后者会将拥有同样监听器的锁唤醒,而前者不会
因为wait方法作用于本身线程,与其他线程无关,而notify方法可以让其他线程调用,来唤醒同样监听器的线程
其中notify是c. notify可以让该类型锁里面的wait唤醒,而wait虽然也是c. wait
但实际上他并没有让其他该类型锁的线程等待,前者可以通过找到wait方法,而进行唤醒
后者却找不到让其他线程等待的东西,即只可以使得当前线程等待
这里"监听器" 实际叫做"同步监听器"
当你认为输出结果与预期不对时,有可能是输出的那个语句没看到了,因为太慢了,即会很少
void wait ( long timeout) ,用于进入等待状态,直到其它线程调用方法或参数指定的毫秒数已经过去为止
即当等待时间到了时,就会自动解除等待,当然,也可以在他等待时直接唤醒
void notify ( ) ,用于唤醒等待的单个线程(如果有多个线程,那么随机选中一个来唤醒,并没有什么顺序,可以认为是伪随机),与wait方法一样,有this 可以不加,有参数,加参数
void notifyAll ( ) ,用于唤醒等待的所有线程,与wait方法一样,有this 可以不加,有参数,加参数
解决wait方法和sleep方法是否会释放锁的问题
public synchronized void run ( ) {
notify ( ) ;
System . out. println ( 1 ) ;
try {
wait ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
System . out. println ( 2 ) ;
}
package com. lagou. task18 ;
public class ConsumerThread extends Thread {
private StoreHouse storeHouse;
public ConsumerThread ( StoreHouse storeHouse) {
this . storeHouse = storeHouse;
}
public ConsumerThread ( ) {
}
@Override
public void run ( ) {
while ( true ) {
storeHouse. consumerProduct ( ) ;
try {
Thread . sleep ( 100 ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
}
}
}
package com. lagou. task18 ;
public class ProduceThread extends Thread {
private StoreHouse storeHouse;
public ProduceThread ( StoreHouse storeHouse) {
this . storeHouse = storeHouse;
}
@Override
public void run ( ) {
while ( true ) {
storeHouse. produceProduct ( ) ;
try {
Thread . sleep ( 1000 ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
}
}
}
package com. lagou. task18 ;
public class StoreHouse {
private int cnt = 0 ;
public synchronized void produceProduct ( ) {
notify ( ) ;
if ( cnt < 10 ) {
System . out. println ( "线程" + Thread . currentThread ( ) . getName ( ) +
"正在生产第" + ( cnt+ 1 ) + "个产品..." ) ;
cnt++ ;
} else {
try {
wait ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
}
}
public synchronized void consumerProduct ( ) {
notify ( ) ;
if ( cnt > 0 ) {
System . out. println ( "线程" + Thread . currentThread ( ) . getName ( ) + "消费第" + cnt + "个产品" ) ;
cnt-- ;
} else {
try {
wait ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
}
}
}
package com. lagou. task18 ;
public class StoreHouseTest {
public static void main ( String [ ] args) {
StoreHouse storeHouse = new StoreHouse ( ) ;
ProduceThread t1 = new ProduceThread ( storeHouse) ;
ConsumerThread t2 = new ConsumerThread ( storeHouse) ;
t1. start ( ) ;
t2. start ( ) ;
}
}
线程池------------------------------
从Java5开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口
常用的方法------------------------------
V call ( ) ,计算结果并返回
FutureTask类------------------------------
public class FutureTask < V >
extends Object
implements RunnableFuture < V >
java.util.concurrent.FutureTask类用于描述可取消的异步计算
该类提供了Future接口的基本实现,包括启动和取消计算
查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果
常用的方法------------------------------
FutureTask ( Callable callable) ,根据参数指定的引用来创建一个未来任务
V get ( ) ,获取call方法计算的结果,并等待线程执行完毕
package com. lagou. task18 ;
import java. util. concurrent. Callable ;
import java. util. concurrent. ExecutionException ;
import java. util. concurrent. FutureTask ;
public class ThreadCallableTest implements Callable {
@Override
public Object call ( ) throws Exception {
int sum = 0 ;
for ( int i = 1 ; i <= 10000 ; i++ ) {
sum += i;
}
System . out. println ( "计算的累加和是:" + sum) ;
return sum;
}
public static void main ( String [ ] args) {
ThreadCallableTest tct = new ThreadCallableTest ( ) ;
FutureTask ft = new FutureTask ( tct) ;
Thread t1 = new Thread ( ft) ;
t1. start ( ) ;
Object obj = null ;
try {
obj = ft. get ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
} catch ( ExecutionException e) {
e. printStackTrace ( ) ;
}
System . out. println ( "线程处理方法的返回值是:" + obj) ;
}
}
线程池的由来------------------------------
在服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了
即每来一个客户端连接,服务器端就要创建一个新线程
如果访问服务器的客户端很多,那么服务器要不断地创建和销毁线程,这将严重影响服务器的性能
概念和原理------------------------------
线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后
就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程
线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程
任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
相关类和方法------------------------------
public class Executors
extends Object
从Java5开始提供了线程池的相关类和接口:java.util.concurrent.Executors类和java.util.concurrent.ExecutorService接口
其中Executors是个工具类和线程池的工厂类,可以创建并返回不同类型的线程池
常用的方法------------------------------
static ExecutorService newCachedThreadPool ( ) ,创建一个可根据需要创建新线程的线程池
static ExecutorService newFixedThreadPool ( int nThreads) ,创建一个可重用固定线程数的线程池
static ExecutorService newSingleThreadExecutor ( ) ,创建一个只有一个线程的线程池
其中ExecutorService接口是真正的线程池接口,主要实现类是ThreadPoolExecutor
常用的方法------------------------------
void execute ( Runnable command) ,执行任务和命令,通常用于执行
Runnable Future < V > submit ( Callable < T > task) ,执行任务和命令,通常用于执行
Callablevoid shutdown ( ) ,启动有序关闭
package com. lagou. task18 ;
import java. util. concurrent. ExecutorService ;
import java. util. concurrent. Executors ;
public class ThreadPoolTest {
public static void main ( String [ ] args) {
ExecutorService executorService = Executors . newFixedThreadPool ( 10 ) ;
executorService. submit ( new ThreadCallableTest ( ) ) ;
executorService. shutdown ( ) ;
}
}
对于io流来说,在多线程的情况下,并不共享文件的下标,每个创建的流是自带自己的下标的(注意是创建的流,如果是同一个流,比如赋值给你,那么自然会共享其状态,所以在某种程度上是一个流一个状态,而大多数是多线程状态,所以也可以说成是多线程情况下,但最终的原因还是是因为不同的流,所以在某些情况下,即多线程的情况并不绝对的),唯一共享的是文件,所以在并发情况下,对于文件的写入,可能存在被覆盖的操作,这里注意即可