1、基本概念(重点)
1.1 进程和线程
1.1.1程序:指令和数据的有序集合,其本身没有任何运行的含义,程序是一个静态的概念。
1.1.2 进程:是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位。进程是一个动态的概念。另外每个进程都有一个独立的内存空间,数据不可与其他进程共享。
一个进程可以包含有多个线程(如视频中同时听到声音、看到图像,还可以看弹幕)
一个进程至少有一个线程,否则无存在的意义。
1.1.3 线程:CPU调度和执行的单位。一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一天天线索,被称为线程。同一条进程中的多条线程,共享一个内存空间,线程之间可以自由切换,并发执行。
1.1.4 线程分类:守护线程与用户线程。
守护线程:是个服务线程,准确地来说就是服务其他的线程,当用户线程全部死亡时,守护线程自动死亡。
用户线程:平时用到的普通线程均是用户线程,当在Java程序中创建一个线程,它就被称为用户线程。
注意:很多多线程是模拟出来,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,cpu只能执行一个代码,因为切换很快,所以就有同时执行的错觉。
面试题1:java中默认开启两条线程main线程和gc线程。
面试题2:提问?JAVA真的可以开启线程吗? 开不了的!
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//这是一个C++底层,Java是没有权限操作底层硬件的
private native void start0();
Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。
1.2 同步与异步
同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
异步:当一个异步过程调用发出后,调用者不能立刻得到结果,可以往下执行。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
1.3 并发与并行
并发: 某个时间段,多条线程操作同一个资源。
CPU 只有一核,模拟出来多条线程,天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。
并行: 某个时间点,多个条线程并行执行。
CPU多核,多个线程可以同时执行。 我们可以使用线程池!
2、 线程的创建
2.1 基础Thread类
步骤:
① 定义类继承Thread;
② 复写Thread类中的run方法;
目的:将自定义代码存储在run方法,让线程运行
③ 调用线程的start方法:
该方法有两步:启动线程,调用run方法
public class Test extends Thread{
public Test(String n){
super(n);
}
/**
* 重写run方法
*/
@Override
public void run() {
for(int i = 0;i<10;i++){
System.out.println("猪八戒:大师兄,不好了,师傅被妖怪抓走了");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String [] args){
Test test = new Test("猪八戒线程");
test.start();
}
}
2.2 实现Runnable接口
步骤:
① 定义类实现Runnable接口
② 覆盖Runnable接口中的run方法
将线程要运行的代码放在该run方法中。
③ 通过Thread类建立线程对象。
④ 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象
⑤ 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
public class TestRunnable extends Thread{
String name;
public TestRunnable(String name){
this.name = name;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0;i<10;i++){
System.out.println(name+",沙师弟:大师兄,不好了,师傅被妖怪抓走了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String [] args){
TestRunnable test = new TestRunnable("沙师弟线程");
test.start();
}
}
2.3 实现Callable接口
步骤:
① 创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
② 创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
③ FutureTask使Runnable接口的一个实现类,所以可以使用FutureTask对象作为参数启动新线程。
④ 调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 1; i < 10; i++) {
// new Thread(new Runnable()).start();
// new Thread(new FutureTask<>( Callable)).start();
MyThread thread= new MyThread();
//适配类:FutureTask
FutureTask<String> futureTask = new FutureTask<>(thread);
//放入Thread使用
new Thread(futureTask,String.valueOf(i)).start();
//获取返回值
String s = futureTask.get();
System.out.println("返回值:"+ s);
}
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Call:"+Thread.currentThread().getName());
return "String"+Thread.currentThread().getName();
}
}
3、线程的六种状态
在枚举类Thread.State中就有六种状态的详细介绍。
NEW
新建状态(new):
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。
RUNNABLE
运行状态 (runnable):
该状态包含就绪(Runnable)和运行(Running)两个状态。
就绪状态:当线程对象调用statr()方法后,线程并不代表它马上运行起来,而是先进入就绪状态。处于就绪状态位于可运行池中,此时它只是具备了运行的额条件,能否获得CPU的使用权,还需要等待系统的调度。
运行状态:如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。
注意:就绪+运行=运行
BLOCKED
阻塞状态 (blocked):
阻塞状态只有在等待进入synchronized方法(块)和 其他Thread调用notify()或notifyAll(),但是还未获得锁才会进入。注意Lock中,是会让线程属于等待状态而不是阻塞,只有synchronized是阻塞。
注意:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态。
WAITING
等待状态(waiting):
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
TIMED_WAITING
超时等待(timed_waiting):
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
TERMINATED
终止状态(terminated):
1.当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
2.在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程状态关系图
4 、线程的调度
概念:Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模式,分别使分时调度模型和抢占式调度模型。
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为 抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
5、常用方法
5.1 线程休眠(sleep)
人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long mills),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。
线程睡眠的方法(两个):
sleep(long millis)在指定的毫秒数内让正在执行的线程休眠。
sleep(long millis,int nanos)在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠。
public class SynTest {
public static void main(String[] args) {
new Thread(new CountDown(),"倒计时").start();
}
}
class CountDown implements Runnable{
int time = 10;
public void run() {
while (true) {
if(time>=0){
System.out.println(Thread.currentThread().getName() + ":" + time--);
try {
Thread.sleep(1000); //睡眠时间为1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//结果:
倒计时:10
倒计时:9
倒计时:8
倒计时:7
倒计时:6
倒计时:5
倒计时:4
倒计时:3
倒计时:2
倒计时:1
倒计时:0
注意:sleep使线程暂停时不释放已获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入到当前同步块或者同步方法中的。
5.2 线程让步(yield)
该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。
public class SynTest {
public static void main(String[] args) {
yieldDemo ms = new yieldDemo();
Thread t1 = new Thread(ms,"张三吃完还剩");
Thread t2 = new Thread(ms,"李四吃完还剩");
Thread t3 = new Thread(ms,"王五吃完还剩");
t1.start();
t2.start();
t3.start();
}
}
class yieldDemo implements Runnable{
int count = 20;
public void run() {
while (true) {
if(count>0){
System.out.println(Thread.currentThread().getName() + count-- + "个瓜");
if(count % 2 == 0){
Thread.yield(); //线程让步
}
}
}
}
}
sleep和yield的区别:
① sleep方法声明抛出InterruptedException,调用该方法需要捕获该异常。yield没有声明异常,也无需捕获。
② sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态。
5.3 线程插队(join)
当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。
join可以用来临时加入线程执行。
public static void main(String[] args) throws InterruptedException {
yieldDemo ms = new yieldDemo();
Thread t1 = new Thread(ms,"张三吃完还剩");
Thread t2 = new Thread(ms,"李四吃完还剩");
Thread t3 = new Thread(ms,"王五吃完还剩");
t1.start();
t1.join();
t2.start();
t3.start();
System.out.println( "主线程");
}
5.4 中断标记(interrupt)
当一个线程被阻塞的时候(io, sleep等),我们取消这种阻塞,这个时候就可以使用interrupt。
package thread;
class Test extends Thread {
boolean stop=false;
public static void main( String args[] ) throws Exception {
Test thread = new Test();
System.out.println( "Starting thread..." );
thread.start();
Thread.sleep( 3000 );
System.out.println( "Interrupting thread..." );
thread.interrupt();
Thread.sleep( 3000 );
System.out.println("Stopping application..." );
//System.exit(0);
}
public void run() {
while(!stop){
System.out.println( "Thread is running..." );
long time = System.currentTimeMillis();
while((System.currentTimeMillis()-time < 1000)) {
}
}
System.out.println("Thread exiting under request..." );
}
}
/**结果
Starting thread...
Thread is running...
Thread is running...
Thread is running...
Thread is running...
Interrupting thread...
Thread is running...
Thread is running...*/
在Thread.interrupt()被调用后,线程仍然继续运行。
使用Thread.interrupt()中断线程 :
Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
6、多线程同步
同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。
线程安全:线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想实现线程安全做到保证某一个共享资源在任何时刻 只能有一个线程访问即可。
为什么使用同步?
多线程给我们带来了很大的方便,但是同时也给我们带来了一个致命的问题,当我们对线程共享数据进行非原子操作时,会带来知名的错误。当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。这就是多线程同步提上议程的原因,解决多线程安全问题。
举个例子:假设银行里某一用户账户有1000元,线程A读取到1000,并想取出这1000元,并且在栈中修改成了0但还没有刷新到堆中,线程B也读取到1000,此时账户刷新到银行系统中,则账户的钱变成了0,这个时候也想去除1000,再次刷新到行系统中,账号的钱变成0,这个时候A,B都取出1000元,但是账户只有1000,显然出现了问题。针对上述问题,假设我们添加了同步机制,那么就可以很容易的解决。
6.1 synchronized同步
synchronized锁对象主要有如下三种:
- 普通同步方法,锁的是当前的实例对象
- 静态同步方法,锁的是当前的Class对象
- 同步代码块锁的是括号里面的对象
这里举一个经典卖票的例子:
package thread;
//线程同步synchronized
public class Demo {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案2 同步方法
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
sale();
}
}
public void sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}
}
}
}
//结果会出现余票为-1 -2等正常现象
6.1.1 synchronized代码块
package thread;
//线程同步synchronized
public class Demo {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案1 同步代码块
//格式:synchronized(锁对象){
//
//
// }
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
}
}
}
}
}
6.1.2 synchronized方法
package thread;
//线程同步synchronized
public class Demo {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案2 同步方法
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
//判断票是否卖完,卖完即结束线程
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;
}
}
}
sychronized可重入锁的实现
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
6.2 Lock同步
概念:
公平锁: 十分公平,必须先来后到;
非公平锁: 十分不公平,可以插队;(Lock默认为非公平锁)
lock三部曲
1、 Lock lock=new ReentrantLock();
2、 lock.lock() 加锁
3、 finally=> 解锁:lock.unlock();
package thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//同步代码块和同步方法都属于隐式锁
//线程同步lock
public class Demo {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案1 显示锁 Lock 子类 ReentrantLock
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
@Override
public void run() {
while (true) {
l.lock();
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
l.unlock();
}
}
}
}
Synchronized 和 Lock区别(注意)
1、Synchronized 内置的Java关键字,Lock是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock可以判断
3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁
4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);
lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。
5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
非公平锁和公平锁在 reetrantlock 里是如何实现的?
非公平实现:
1.调用lock()方法时,首先去通过CAS尝试设置锁资源的state变量,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
2.调用tryAcquire方法时,首先获取当前锁资源的state变量,如果为0,则通过CAS去尝试设置state,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
以上两步都属于插队现象,可以提高系统吞吐量
公平实现:
1.调用lock()方法时,不进行CAS尝试
2.调用tryAcuqire方法时,首先获取当前锁资源的state变量,如果为0,则判断该节点是否是头节点可以去获取锁资源,如果可以才通过CAS去尝试设置state
上面通过判断该线程是否是队列的头结点,从而保证公平性
6.3 死锁
进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。
public class DeadLock {
public static void main(String[] args) {
Thread t1 = new Thread(new DeadLockTest(true));
Thread t2 = new Thread(new DeadLockTest(false));
t1.start();
t2.start();
}
}
class DeadLockTest implements Runnable{
private boolean flag;
static Object obj1 = new Object();
static Object obj2 = new Object();
public DeadLockTest(boolean flag) {
this.flag = flag;
}
public void run(){
if(flag){
synchronized(obj1){
System.out.println("if lock1");
synchronized (obj2) {
System.out.println("if lock2");
}
}
}else{
synchronized (obj2) {
System.out.println("else lock2");
synchronized (obj1) {
System.out.println("else lock1");
}
}
}
}
}
死锁形成的必要条件总结(都满足之后就会产生):
① 互斥条件:资源不能被共享,只能被同一个进程使用;
② 请求与保持条件:已经得到资源的进程可以申请新的资源;
③ 非剥夺条件:已经分配的资源不能从相应的进程中强制剥夺;
④ 循环等待条件:系统中若干进程形成环路,该环路中每个进程都在等待相邻进程占用的资源。
7、线程通信
线程通信的目的是为了能够让线程之间相互发送信号。另外,线程通信还能够使得线程等待其它线程的信号,比如,线程B可以等待线程A的信号,这个信号可以是线程A已经处理完成的信号。
方法声明 | 功能描述 |
---|---|
void wait() | wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒(进入“就绪状态”) |
void notify() | 唤醒当前对象上的等待线程,notify()是唤醒单个线程。 |
void notifyAll() | 唤醒当前对象上的等待线程,notifyAll()是唤醒所有的线程。 |
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{for(int i=0;i<10;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{for(int i=0;i<10;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"B").start();
}
}
class Data{
//数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
if(number!=0){
//等待操作
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程 我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if(number==0){
//等待操作
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程 我-1完毕了
this.notifyAll();
}
}