一锁两并三程
锁
synchronized
并发和并行
并发(concurrent):在一台处理器上“同时”处理多个任务,即有多个任务在单个cpu上交替进行,但其实在同一时刻,只有一个任务在执行。
并行(parallel):在多台处理器上同时处理多个任务,即同一时刻,有多个指令在多个cpu上同时执行。例子:锅在加热的同时你在切菜。
2核4线程:可以最多同时运行4个线程,超过4个线程,则需要在这些线程间交替执行
进程、线程、管程
进程:在系统中运行的一个应用程序就是一个进程,每一个进程都有他自己的内存空间和系统资源。
例子“每个软件就是一个进程,idea、mindmanager2016、sublime、任务管理器都是进程
线程:也被称为轻量级进程,在同一个进程内会有1个或多个线程,是大多数操作系统进行时序调度的基本单元。
例子:idea这个进程下有很多个线程
管程:Monitor(监视器),也就是我们平时所说的锁。监视器是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
例子:使用synchronized时,需要传入一个对象,此时传入的对象o就作为监视器
总结:
执行线程要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
多线程的实现方式
- 继承Thread类的方式进行实现
- 实现Runnable接口的方式进行实现
- 利用Callable接口和Future接口方式实现
三种方式对比
Thread类常用的方法
- setName和getName
设置线程名字的方法:
- 如果没有给线程设置名字,线程的默认名字为Thread-X(X为序号,从0开始)
注意:main方法所在的线程的名字为main
- 可以用setName方法设置线程名字,
- 可以在Thread的构造方法中设置线程的名字
注意:使用构造方法设置线程名字时需要在实现类中构造方法中调用父类的构造方法
- currentThread
currentThread可以获取当前线程的对象,哪条线程执行到这个方法,此时获取的就是那条线程的对象
当JVM虚拟机启动之后,会自动的启动多条线程,其中有一条线程就叫做main线程,他的作用就是去调用main方法,并执行里面的代码。在以前,我们写的所有的代码,其实都是运行在main线程当中
- sleep
sleep方法可以让线程休眠指定的时间,单位为毫秒(1秒=1000毫秒)。哪条线程执行到这个方法,那么那条线程就会在这里停留对应的时间。 当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
- setPriority和getPriority
cpu调度方式:
- 抢占式调度
多个线程抢夺cpu的执行权,cpu执行哪条线程是不确定的,执行多长时间也是不确定的,抢占式调度具有随机性。
jvm采用抢占式调度方式,所以每个线程何时执行,执行多长时间都是不确定的,线程的优先级越高,抢到cpu执行权的概率越大。
- 非抢占式调度
setPriority方法用来设置线程的优先级,优先级默认为5。优先级范围为1-10。
注意:
main线程的默认优先级也是5
- setDaemon
setDaemon方法用于设置守护线程,setDaemon方法必须用在start方法前。
女神线程执行完毕后,备胎线程没有打印到“备胎@100”就提前结束了
- isDaemon
判断是否为守护线程
守护线程的应用场景:
-
垃圾回收线程
-
聊天的同时传输文件。聊天窗口为线程1,传输文件为线程2。关闭聊天窗口(关闭线程1)后,传输文件也会自动断开(关闭线程2)。此时可以把传输文件设置为守护线程,当关闭线程1后,线程2就会自动断开了。
- yield
yield方法会出让cpu的执行权
- join
注意:
相当于把线程t加入到了main线程中,即把两个线程合并为单线程
线程的生命周期
**问:**sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?
不会,阻塞状态结束后会进入就绪状态,而不是运行状态
线程的安全问题
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
出现的问题:
问题1:三个窗口同时卖同一张票
问题2:窗口1和窗口3卖的票超出了100张
原因:
一开始ticket=0,窗口1这个线程运行到if(ticket<100)时,符合条件,窗口1进入if的方法体内,运行到Thread.sleep(100)时,窗口1会进入阻塞状态,丢失cpu的执行权,此时cpu的执行权会被窗口2或窗口3抢走,假设窗口2抢到了执行权,此时ticket仍然是2,所以窗口2也会进入到if方法体内,运行到Thread.sleep(100)时,窗口2进入阻塞状态,假设此时窗口1仍处于阻塞状态,此时执行权一定会被窗口3抢走,同样的,此时ticket=2,窗口3运行Thread.sleep(100)时,窗口3进入阻塞状态。假设执行权又被窗口1抢走,窗口1执行完ticket++这条语句,此时ticket=1,碰巧窗口1在执行sout这条语句前,执行权被窗口2抢走,窗口2执行到ticket++后,此时ticket=2,碰巧窗口2执行sout这条语句前,执行权又被窗口3抢走,窗口3执行完ticket++后,此时ticket=3,窗口3继续向下执行,执行sout语句,此时输出“窗口3正在卖第3张票”,执行完sout这条语句后,执行权又被窗口1抢走,窗口1执行sout语句,此时输出“窗口1正在卖第3张票”,窗口2同理…
注意:
线程在执行任意语句后,执行权都概率被其他线程抢走,并不是线程进入阻塞状态后才有概率被其他线程抢走!Thread.sleep(100)只是为了方法这个现象!
解决办法:
办法1:同步代码块
把操作共享数据的代码锁起来
格式:
java">synchronized (锁对象) {操作共享数据的代码}
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
同步代码块实现卖票:
办法2:同步方法
就是把synchronized关键字加到方法上
格式:
java">修饰符 synchronized 返回值类型 方法名(方法参数) {...}
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定
非静态方法的锁对象是:this
静态方法的锁对象是:当前类的字节码文件对象
同步方法使用技巧:
先写同步代码块,然后将同步代码块改成同步方法
同步方法实现卖票:
- 先写成同步代码块的形式
- 将同步代码块抽取成同步方法
注意:MyRunnable实现了Runnable接口,开启多线程时使用MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr);
的方式,因为只创建了一个MyRunnable()对象,三个线程传入了相同的MyRunnable()对象,int ticket不需要加static关键字,因为三个线程访问的是同一个对象的ticket。
Lock锁
实现方式
死锁
男孩(线程1)和女孩(线程2)一起抢饭吃,但是只有两只筷子:筷子A(锁A)和筷子B(锁B)。规则:只有一个人同时拿到两只筷子才能吃饭,吃一口饭后要把筷子放到桌子上(释放锁),两个人重新抢筷子吃饭。
情况1:男孩(线程1)同时拿到筷子A(锁A)和筷子B(锁B),吃一口后放到重新将筷子A(锁A)和筷子B(锁B)桌子上。
情况2:女孩(线程2)同时拿到筷子A(锁A)和筷子B(锁B),吃一口后放到重新将筷子A(锁A)和筷子B(锁B)桌子上。
情况3:男孩(线程1)拿到筷子A(锁A),女孩(线程2)拿到筷子B(锁B),两个人都等待对方吃一口饭后放下筷子(释放锁)
情况4:男孩(线程1)拿到筷子B(锁B),女孩(线程2)拿到筷子A(锁A),两个人都等待对方吃一口饭后放下筷子(释放锁)
情况3和情况4就发生了死锁错误。
生产者和消费者(等待唤醒机制)
常见方法:
需求:
完成生产者和消费者(等待唤醒机制)的代码,实现线程轮流交替执行的效果
多线程的状态
注意:
这些状态是运行在jvm态,并没有反应所有操作系统线程状态。
线程抢到cpu的执行权后,jvm就会把该线程交给操作系统,所以jvm上线程是没有运行状态的。
线程池
线程池的主要原理:
创建线程池的方法:
使用线程池工具类:Executors。通过调用Executors的静态方法来创建不同类型的线程池对象。
使用线程池:
- 创建没有大小限制的线程池(线程池的大小为int所能表述的最大值)
- 创建固定大小的线程池
自定义线程池
自定义线程池的7个参数:
注意1:
临时线程对象的最大数量=线程池中最大线程的数量-核心线程数量;
空闲时间指的是临时线程对象多长时间没有工作,则会将临时线程对象销毁。
注意2:
来任务了需要先创建核心线程对象,核心线程对象都是工作状态,再来任务的话会进入核心线程的队列中等待,核心线程队列满了才会创建临时线程对象;
线程执行顺序不是按照先来后到的规则,队伍里的线程会按照先来后到的规则执行等待核心线程执行,而队伍外的线程会使用临时线程执行;
核心线程对象都是工作状态,核心线程队伍满了,临时线程对象也都是工作状态,此时再来任务,会触发任务拒绝策略,默认拒绝策略是舍弃不要。
任务拒绝策略:
代码实现: