1 什么是线程
线程是操作系统中能过被运行调度得最小得单位,他被包含在进程之中,是进程得执行单位,一条线程指的是进程中一个单一顺序的控制流。一个进程可以并发多个线程,而多个线程可以并行执行不同得任务,总结就是:
- 进程:是应用程序进入内存中,就是一个内存中运行的应用程序,比如QQ ,微信,网易云音乐
- 线程:线程属于进程,是进程的一个执行单元,负责程序的执行。(应用程序通向CPU的执行路径,CPU可以通过这个路径执行功能,这个功能又叫线程。)
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 一个程序至少有一个进程,一个进程又至少有一个线程
- 线程的划分程度小于进程,使得多线程程序的并发性更高
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
分时调度:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
创建线程
创建线程有四种方法,继承Thread类,实现Runnable接口,这两种方式都是没有返回值的,而实现Callable接口是具有返回值的,并且可以抛出异常,第四种就是使用线程池来创建了,需要的时候直接用就可以了,减少资源的消耗,这篇博客有写的哦 ==> Java创建线程的四种方式。
2 并发和并行
- 并发:两个事件或多个事件交替执行,在同一个时间段内发生。 重点在于他们是交替发生的,轮流交替,比如你一边喝水一遍吃零食,宏观上看你是同时在吃东西和喝水,但是实际上你是交替进行的,同一个时刻是做一件事情。
- 并行:两个事件或多个事件在同一时刻发生 (同时发生), 重点是他们是同一个时刻发生的,比如你边写字边唱歌,这两件事情是在同一时刻发生的,这个就是并行,同时进行。
3 线程的状态
在Java中,线程被分成了六个状态,而在操作系统中,线程是被分成五个状态的,我们可以从java.lang.Thread.State类中可以看出:
public enum State {
/**
* Thread state for a thread which has not yet started.
* 线程的新生状态
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
* 线程运行中/等待
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
* 线程的阻塞状态
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
* 线程的等待态,死等,等,就应等
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
* 线程超时等待状态,超过一定的时间,就不在等待了
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
* 线程中止状态,代表线程执行完毕
*/
TERMINATED;
}
也就是说Java线程是由六个状态的:
- NEW:新建状态
- RUNNABLE:就绪/运行
- BOLCKED:阻塞状态
- WAITING:等待态
- TIMED_WAITING:超时等待
- TERMINATED:终止态
而操作系统的线程是只有五个状态的:
- NEW:初始状态,对应Java中的
NEW
状态 - READY:可运行状态,对应 Java中的
RUNNBALE
状态 - RUNNING:运行状态,对应 Java中的
RUNNBALE
状态 - WAITING:等待状态,该状态在 Java中被划分为了
BLOCKED
,WAITING
,TIMED_WAITING
三种状态 - TERMINATED:终止状态
名称 | 解释 |
---|---|
NEW | 当一个线程被new出来的时候,还没有执行start方法,就会处于该状态 |
RUNNABLE | Java将线程中的就绪态(READY )和运行态(RUNNING )统一为这一种状态了,就是一个线程调用start方法后,要么是等待调度,要么是正在运行,一个线程被启动后,就会处于就绪态(READY ),等待CPU的调度,只有CPU给他分配了时间片,他才会处于运行态(RUNNING )。 |
BOLCKED | 通常和锁有关系,表示线程正在尝试获取被上锁的资源,比如进入synchronized修饰的方法和代码块 |
WAITING | 当该线程执行wait方法或者join方法时,不带参数就表示永久睡眠,只能被其他线程唤醒,被唤醒后,看CPU是否空闲从而进入不同的状态 |
TIMED_WAITING | 调用wait方法或者是sleep方法,这个是携带参数的,睡眠指定的时间,在这里我们要注意的是,sleep方法是不会释放掉锁的,而wait方法会释放自己所占的锁资源的,所以说调用sleep方法后,时间到了,线程自动唤醒,就会处于RUNNABLE 状态的花,否则,就要根据CPU是否空闲来决定处于哪一个状态了。 |
TERMINATED | 线程run方法完成了,或者说main方法结束了,我们就认为他是终止了,要注意的是,也许这个线程对象还存在,但是他不在是一个可以单独执行的线程了,线程一旦终止了,就不可以复生,就不可以在执行start方法啦, 当然,一个线程任何时候只能调用一次start方法,再一次调用的话,就会抛出IllegalThreadStateException 异常。 |
4 Synchronized 和 lock锁
synchronized
锁是什么
synchronized
是Java中的一个关键字,是一个同步锁,我们为什么要使用他呢,在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized
可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized
可以保证一个线程的变化可见(可见性),即方法或者代码块在运行的时候只有一个能够进入临界区,他有三种应用方式:
- 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
- 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
- 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
同步代码块中锁对象
synchronized
代码块的使用方法如下:
synchronized(obj){ //obj是我们想要锁住的对象
// 中间写执行的代码
}
这个锁住的对象是会有三种情况的:
- this,也就是当前调用这个方法的对象
- 类的class模板,也就是我们实例化对象的那个类
- 我们自己定义的一个对象,可以是一个Sting,也可以是一个Object类型的。
synchronized
同步代码块的锁的粒度会更加的小,可以锁住我们想要的代码,对于这一部分进行同步进行的要求,那来讲一下三个不同锁对象的区别:
- 自己定义的对象的,就是我们可以划分出很多个代码块,分别加上不同的锁,这个时候只有锁对象是同一个对象的时候才会进行锁资源的抢夺,锁不同的话,是不会进行资源的抢夺的,或者说,一个对象,调用两个不同的方法,他们锁对象都是方法里面创建的名称为lock的对象,这个时候,调用这两个方法也是会异步执行的,两个方法是没有影响的,因为他们是两个锁对象。
- this,是方法的调用者,也就是说,一个对象,调用了两个不同的方法,但是他们的锁对象是this的话,他们也是会发生阻塞的,进行锁资源的抢夺,因为他们锁的对象是方法的调用者。
- class模板,class模板是一个类只有一份的,也就是说,如果两个对象调用不同的方法,但是他们的代码块的锁对象是class模板,那么他们也是会阻塞的,因为这个锁只有一份,是类的。
8锁问题
八锁问题就是synchronized
锁的八种情况,对于多个线程执行不同的同步方法,静态方法,多个对象,然后线程是如何进行方法的调用的,锁的对象都是什么,锁是什么,都有什么区别,这一篇博客都有讲的哦,可以去看一哈子 ===> 八锁现象的简单介绍
synchronized
锁的缺陷
- 被
synchronized
锁修饰的代码块或者方法,一旦拿到了对应的锁,那么释放锁资源的方式只有执行完了代码块释放,或者出现异常,jvm让线程自动释放锁,但是这个时候如果这个获取锁的线程被阻塞了,没有及时释放锁,那么其他需要这个锁的线程只可能干巴巴的等着这个锁,那么这个操作是十分消耗系统资源的,而lock可以自主释放锁,不让线程无休止的等下去。 - 多个线程读写一个文件时,应该是读读操作是兼容的,读写和写写操作是不兼容的,但是使用
synchronized
的话,读读操作都是不被允许的。
lock 锁是什么
lock锁,他是在jdk1.5之后,java.util.concurrent.locks
包下的另外一种实现同步访问的技术,这个时候就会问了,有了synchronized
锁了,为什么还有一个lock锁呢,因为lock锁可以做到synchronized
锁做不到的东西,它可以很好的解决掉上面 synchronized 锁的缺陷。
通过查看他的源码可以知道,lock是一个接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
那现在我们来对这六个方法来做一个解释
方法名 | 解释 |
---|---|
lock() | lock() 是最基础的获取锁的方法。 在线程获取锁时如果锁已被其他线程获取,则进行等待,是最初级的获取锁的方法,对于lock接口而言,获取锁和释放锁都是显示的,所以我们必须手动的去获取锁,通常获取操作我们在try{}代码块中进行 |
unlock() | unlock()是我们手动释放锁的方式,我们释放锁操作是在finally中进行的,所以说正常的使用流程就是,try{}中获取锁,并且执行操作。cath()中检查异常抛出,finally{}中释放锁资源,尤其要注意的一点是,我们是必须要释放锁资源的,否则就会容易产生死锁。 |
tryLock() | tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false,代表获取锁失败。相比于 lock(),这样的方法显然功能更强大,可以根据是否能获取到锁来决定后续程序的行为 ,该方法的返回结果是会立即返回的,所以我们通常和if()语句一起使用 |
tryLock(long time, TimeUnit unit) | tryLock() 的重载方法是 tryLock(long time, TimeUnit unit),这个方法和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true,这个方法就可以避免死锁的问题,因为我获取不到锁并且我的等待时间到了,那么我就会自己主动的释放锁,我不要了,诶,就是玩。 |
lockInterruptibly() | 这个方法的作用就是去获取锁,如果这个锁当前是可以获得的,那么这个方法会立刻返回,但是如果这个锁当前是不能获得的(被其他线程持有),那么当前线程便会开始等待,除非它等到了这把锁或者是在等待的过程中被中断了,否则这个线程便会一直在这里执行这行代码。一句话总结就是,除非当前线程在获取锁期间被中断,否则便会一直尝试获取直到获取到为止 ,它就相当于是一个无限等待的trylock()方法 |
newCondition() | 获取condition对象,可以执行对应的await()方法和signal()方法来进行线程间的通信 |
这里我们用一段代码来看看这些方法是如何使用的:
public class MyLock {
private Lock lock = new ReentrantLock(); //定义一个全局的锁
public static void main(String[] args) {
final MyLock myLock = new MyLock(); //定义一个类调用方法
new Thread(()->myLock.test(),"A").start();
new Thread(()->myLock.test(),"B").start();
}
public void test(){
//lock.lockInterruptibly();
//可以在代码一开始就使用这个,直到获取到锁了,才执行,否则一直等待
while (true){
if(lock.tryLock()){ //尝试获取锁,得到啦
System.out.println(Thread.currentThread().getName()+" ==> 获取到了锁");
try {
//因为trylock尝试获取锁,返回true时就已经得到锁了,所以这里不需要在上锁,但是一般都是在try里面加锁
//lock.lock();
System.out.println(Thread.currentThread().getName()+"==> 我给自己加上lock锁");
Thread.sleep(10);
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"==> 释放了lock锁");
lock.unlock(); //finally 里释放锁
break;
}
}else{
System.out.println(Thread.currentThread().getName()+" ==> 还在苦苦等待");
try {
TimeUnit.MICROSECONDS.sleep(1000); //让程序睡眠1ms,便于观察结构
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
从上图我们可以看出,我们开启了两个线程,线程A首先获取锁,然后这个时候休眠,那么线程B就会被调用,但是他一直获取不到锁,所以一直在苦苦等待,直到A释放了锁,这个时候线程B才可以拿到锁,释放。
lock 和 synchronized 的区别
- lock不是java语言内置的,synchronized是java语言的关键字,因此是内置特性。lock是一个类,通过这个类可以实现同步访问。
- ck和synchronized有一点非常大的不同,采用synchronized不需要用户手动的去释放锁,当synchronized方法或者代码块执行完毕之后,系统会自动的让线程释放对锁的占有,而lock则必须要用户去手动释放锁,如果没有主动的释放锁,就会可能导致出现死锁的现象。
5 线程通信
多个线程并发执行的时候,在CPU中是随机切换的,这个时候我们想多个线程一起来完成一件任务,这个时候我们就需要线程之间的通信了,多个线程一起来完成一个任务,线程通信一般有种方式。
- 利用
volatile
关键字 - 利用 Object类的 wait/notify 方法
- 通过 condition 的 await/signal 方法
- 通过 join 的方式
这个我两天内会写一篇比较详细的博客的,啦啦啦啦,现在晚上,休息,先把这一篇发出来。
6 线程中的常用方法
方法名 | 解释 |
---|---|
start() | 线程启动 |
sleep(long millis) | 线程睡眠指定时间 |
getName()() | 返回线程的名字 |
wait() | 线程等待,直到其他线程调用notify()方法唤醒,他们的锁必须是同一个 |
notify() | 随机唤醒一个线程,他们的锁必须是同一个 |
notifyAll() | 会唤醒所有线程,他们的锁必须是同一个 |
join() | 插队,例如A线程调用了B线程的join方法,那么只有B线程结束了,A线程才可以继续运行 |
yield() | 就是当前线程会让出CPU,然后开始再一次抢夺,可能他又一次抢到了CPU,反正就是我让了,你们抢不到,怪我咯 |
setPriority() | 设置线程的优先级,数字越大,优先级越高,优先级是1-10,要注意的是并不是线程优先级越高,就一定会先执行,只是概率会很大 |
小结
线程的内容我现在写的就是这些了,为什么写了,最主要的还是因为我自己也不是特别了解了,在熟悉一下,加深印象,当然,写的不好,希望可以指出来哦,后面会逐渐完善,加新的内容滴。