线程
进程和线程
进程:重量级,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。启动进程消耗的内存比启动线程消耗的内存要高得多。
线程:轻量级,线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,也可以有多个线程。缺点:多个线程是先用进程的共享资源,需要对共享资源进行同步,确保一个共享资源最多只能服务于一个线程。
线程的串行、并行和并发
并行:同时执行。
并发:同一时间执行多个线程,并不是同时执行,而是cpu在多个线程中高速切换。
创建线程的方式:
1)继承Thread类:
class testThead extends Thread {
public void run(){
for(int i = 0;i<10;i++){
//currentThread返回当前执行线程的引用,getName:返回线程的名称
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Test{
public static void main(String[] args) {
Thread a = new Thread(new testThead(),"a线程");
Thread b = new Thread(new testThead(),"b线程");
//通过调用start()方法开启线程,线程开启后会自动调用线程的run()方法。
a.start();
b.start();
}
}
2)实现Runnable接口:
实现Runnable接口,一个类可以实现多个接口,程序弹性和灵活性比继承Thread高。
/**
* 规范:实现Runnable的类型命名以Task结尾
* DeskTask多线程中的一个任务
* run()方法就是任务体
*/
public class DeskTask implements Runnable {
@Override
public void run() {
for(int i=1;i<=10;i++) {
System.out.println(Thread.currentThread().getName()+"::搬了第"+i+"个桌子");
}
}
}
/**
*在主线程中启动桌子和椅子步骤:
*1创建Thread类型的对象
*2将“任务”注入到线程(Thread)中
*3调用Thread类型的start()方法
*任务注入到线程
*此时main(主线程)启动桌子和椅子线程完毕就会出栈,不会守护桌子和椅子线程
*main线程不是守护线程
*/
public class TestRunnable {
public static void main(String[] args) {
//将桌子任务注入到线程
Thread t1 = new Thread(new DeskTask());
t1.setName("桌子线程");
t1.start();
//将椅子任务注入到线程
Thread t2 = new Thread(new ChairTask());
t2.setName("椅子线程");
t2.start();
}
}
3)匿名内部类创建线程:
public class TestThreadAddSub {
//匿名内部类创建线程
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"这是第一个线程");
}
}).start();
new Thread(()-> {
System.out.println(Thread.currentThread().getName()+"这是第二个线程");
}).start();
}
}
线程的生命周期:
当线程被创建并启动以后,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种不同的状态。
新建状态(New)
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。处于新建状态的线程有自己的内存空间,通过调用start方法进入就绪状态(Runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则抛出IllegalThreadStateException异常。
就绪状态(Runnable)
处于就绪状态的线程已经具备了运行条件(也就是具备了在CPU上运行的资格),但还没有分配到CPU的执行权,处于“线程就绪队列”,等待系统为其分配CPU。 就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。 一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
运行状态(Running)
处于就绪状态的线程,如果获得了CPU的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。
运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
如果该线程失去了CPU资源,就会又从运行状态变为就绪状态,重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出CPU资源,再次变为就绪状态。
阻塞状态(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态。
2.同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
3.其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
线程的优先级:
可以为每个线程设置优先级,优先级高的线程任务先执行完毕的概率就会越大,不是优先级最高的线程一定会先抢到CPU。最小的优先级(1)、最大的优先级(10)、普通的优先级(5),线程默认优先级是5,工作中线程优先级只能是1、5、10。
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
设置线程优先级方法:
线程名.setPriority(优先级常量);
sleep():
如果我们需要让当前正在执行的线程暂停一段时间
并进入阻塞状态,指定时间之后,解除阻塞状态,进入就绪状态,则可以通过调用Thread的sleep方法,sleep方法是静态方法。
语法:
正在运行状态的线程对象.sleep();
1.它只对正在运行状态的线程对象有效。
2.使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
yield():
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出CPU资源给其它的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。
正在运行状态的线程对象.yield();
join():
线程的合并就是:线程A在运行期间,可以调用线程B的join()方法,这样线程A就必须等待线程B执行完毕后,才能继续执行。在主线程中,将其它线程合入到主线程中,其它线程执行完毕之后最后结束主线程。
线程的停止
如何停止正在运行的线程?
使用定时器去停止正在执行的线程,定时器是一个监听者,监听线程
定时器:设定一个周期,按照指定的周期重复的执行任务
定时器有一个定时任务(实现了Runnable接口)
场景:main线程中启动一个ThreadA,同时开启一个定时任务去监听ThreadA,定时任务每间隔5秒钟扫描当前工程有没有stop文件,如果有将ThreadA线程停止掉
public class TestStop {
public static void main(String[] args) {
StopTask stop = new StopTask(true);
Thread stopThread = new Thread(stop);
stopThread.start();
//创建定时器,定时器包含一个定时任务,每个5秒钟检查当前工程下面有没有stop文件,如果有停止线程
Timer timer = new Timer();
//schedule()方法调度定时任务的执行
timer.schedule(new TimerTask() {
@Override
public void run() {
File file = new File("stop");
//条件成立:表示stop文件存在,停止线程
if(file.exists()) {
stop.setFlag(false);
//StopTask任务停止,立马删除stop文件
file.delete();
//取消定时任务
timer.cancel();
}
}
}, 1000,5000);
}
}
class StopTask implements Runnable{
private boolean flag ;
public void setFlag(boolean flag) {
this.flag = flag;
}
public StopTask(boolean flag){
this.flag = flag;
}
@Override
public void run() {
long index=0;
for(;flag;) {
index++;
if(index%500000000==0) {
System.out.println(Thread.currentThread().getName()+":::"+index);
}
}
}
}public class TestTimerr {
public static void main(String[] args) {
Timer timer = new Timer();
//参数1:定时器里面的定时任务,参数2:delay延迟多长时间执行
// timer.schedule(new TimerTask() {
// @Override
// public void run() {
// System.out.println(new Date());
// }
// }, 1000);
//Java的定时器:指定一个周期重复执行
//参数1:定时器里面的定时任务
//参数2:delay延迟多长时间执行(此时表示当前时间之后的1秒钟开始执行定时任务)
//参数3:每次任务执行的间隔周期(此时表示每个2秒执行一次定时任务)
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(new Date());
}
}, 1000,2000);
}
}
守护线程:
当正在运行的线程都是守护线程时,Java虚拟机将退出。setDaemon(true)方法必须在启动线程前调用,否则抛出IllegalThreadStateException异常。
如果我们不将一个线程以守护线程方式来运行,即使主线程已经执行完毕,程序也永远不会结束。
注意:如果守护线程守护的是main的主线程,只有当其运行的进程结束,main的主线程才会结束。
线程同步:
线程安全:
非线程安全:多个线程同一时刻访问一个共享资源,造成共享资源的数据出现脏数据,不安全(数组下标越界)。
线程安全:需要对共享资源上一把锁,确保同一时刻最多只能有一个线程访问共享资源,其他线程在外面等待,不会出现脏数据。
synchronized关键字:
同步机制:步骤:A,B,C三个线程同时访问一个共享资源,A线程访问共享资源,判断有没有线程进入,如果没有访问共享资源,B线程访问共享资源,判断有没有线程进入,如果有线程进入了共享资源将在锁池中等待,A线程访问完毕了,会将锁的钥匙放到锁池中,B线程拿到钥匙就可以进入了。
synchronize就是为共享资源上锁,确保同一时刻最多只能有一个线程访问共享资源
例:创建3个线程,同时访问一个ArrayList。
public class TestArrayList {
public static void main(String[] args) {
ArrayList<Integer> list =new ArrayList<>();
new Thread(()->{
while(true) {
list.add(1);
}
}).start();
new Thread(()->{
while(true) {
list.add(1);
}
}).start();
new Thread(()->{
while(true) {
list.add(1);
}
}).start();
new Thread(()->{
while(true) {
list.add(1);
}
}).start();
}
}
public class ArrayList<E> {
private int size;
private Object[] dataElement;
private final Resource resource = new Resource();
public ArrayList(int capacity) {
dataElement = new Object[capacity];
}
public ArrayList() {
this(10);
}
public int size() {
return size;
}
@SuppressWarnings("unchecked")
public E get(int index) {
if(index<0 || index>=size) {
throw new ArrayIndexOutOfBoundsException("index="+index+":size="+size);
}
return (E)dataElement[index];
}
public void add(E element) {
//不会改变数组的大小,多个线程可以同时访问
int length = dataElement.length;
//被synchronize修饰的代码叫做临界区(临界区的代码块最多只能有一个线程访问)
synchronized (resource) {
//条件成立:数组没有空间了,需要扩容
if(size==length) {
int newLength = length+(length>>1);
dataElement = Arrays.copyOf(dataElement, newLength);
}
dataElement[size++] = element;
}
}
}
小结:
1工作中共享资源必须加锁
2为了提供程序在内存中运行的效率,尽量不要锁住整个方法,而是锁代码块
3 那些代码块需要上锁?
first:某个共享资源的数据经常会发生改变
second:被多个线程使用
4 使用synchronize修饰某个块代码,叫做同步块
5 被synchronize修饰的代码叫做临界区(临界区的代码块最多只能有一个线程访问)
6 synchronize是一个JVM级别的互斥锁(排它锁)
JVM帮你加锁和解锁(不是人为加锁和解锁)
缺点:一旦出现异常可能无法解锁,因为不是你手工加锁
7 同步块括号不能少,括号里面的对象表示你要锁住的共享资源,最好使用final修饰,因为不可改变的
8 synchronize括号里面的对象不支持基本类型
Lock关键字:
比synchronize更加友好,一旦出现异常可以人工解锁,特征:有一个公平机制,等待时间最长的线程优先进入共享资源。
//true:启动公平锁 false:非公平锁 默认false
private Lock lck = new ReentrantLock(true);
//加锁
lck.lock();
try{
}finally{
//解锁
lck.unlock();
}
//好处:一旦try块出现异常执行finally,将锁解掉
//解锁操作工作中一定要放在finally块中,出现异常立马解锁。
//工作中如果使用Lock,一定先编写整体(try....finally块),再编写局部(try块里面的内容)
例子:创建3个线程,同时访问一个ArrayList。
public class TestArrayList {
public static void main(String[] args) {
ArrayList<Integer> list =new ArrayList<>();
new Thread(()->{
while(true) {
//写操作
list.add(1);
}
},"Add线程").start();
new Thread(()->{
while(true) {
//读操作
list.get(0);
}
},"Get线程").start();
new Thread(()->{
while(true) {
//读操作
list.size();
}
},"Size线程").start();
}
}
public class ArrayList<E> {
private int size;
private Object[] dataElement;
/**
* true:公平的重入锁
*/
private Lock lck = new ReentrantLock(true);
public ArrayList(int capacity) {
dataElement = new Object[capacity];
}
public ArrayList() {
this(10);
}
public int size() {
lck.lock();
try {
return size;
} finally {
lck.unlock();
}
}
@SuppressWarnings("unchecked")
public E get(int index) {
lck.lock();
try {
if(index<0 || index>=size) {
throw new ArrayIndexOutOfBoundsException("index="+index+":size="+size);
}
return (E)dataElement[index];
} finally {
lck.unlock();
}
}
public void add(E element) {
//不会改变数组的大小,多个线程可以同时访问
int length = dataElement.length;
lck.lock();
try {
//条件成立:数组没有空间了,需要扩容
if(size==length) {
int newLength = length+(length>>1);
dataElement = Arrays.copyOf(dataElement, newLength);
}
dataElement[size++] = element;
} finally {
lck.unlock();
}
}
}
读写锁:
读写锁:由读锁(如果你为多个方法加了读锁,操作多个读方法的线程可以同时进入临界区)和写锁(如果多个方法加上了写锁,最多只能有一个线程进入临界区)组成。
读:并行,一旦操作共享资源多个线程的读方法进入临界区,所有的操作写方法的线程必须在外面等待
写:串行,一旦某个线程进入了贡献在资源的写方法临界区,所有的读方法在外面等待,其他的操作写方法的线程也将在外面等待。
例子:创建3个线程,同时访问一个ArrayList。
public class ArrayList<E> {
private int size;
private Object[] dataElement;
/**
* true:公平的重入读写锁
*/
private ReadWriteLock rw = new ReentrantReadWriteLock(true);
public ArrayList(int capacity) {
dataElement = new Object[capacity];
}
public ArrayList() {
this(10);
}
public int size() {
//添加读锁
//rw.readLock() 获取读写锁里面的读锁
rw.readLock().lock();
try {
return size;
} finally {
rw.readLock().unlock();
}
}
@SuppressWarnings("unchecked")
public E get(int index) {
rw.readLock().lock();
try {
if(index<0 || index>=size) {
throw new ArrayIndexOutOfBoundsException("index="+index+":size="+size);
}
return (E)dataElement[index];
} finally {
rw.readLock().unlock();
}
}
public void add(E element) {
//不会改变数组的大小,多个线程可以同时访问
int length = dataElement.length;
//上读写锁里面的写锁(串行)
rw.writeLock().lock();
try {
//条件成立:数组没有空间了,需要扩容
if(size==length) {
int newLength = length+(length>>1);
dataElement = Arrays.copyOf(dataElement, newLength);
}
dataElement[size++] = element;
} finally {
rw.writeLock().unlock();
}
}
}
public class TestArrayList {
public static void main(String[] args) {
ArrayList<Integer> list =new ArrayList<>();
new Thread(()->{
while(true) {
//写操作
list.add(1);
}
},"Add线程").start();
new Thread(()->{
while(true) {
//读操作
list.get(0);
}
},"Get线程").start();
new Thread(()->{
while(true) {
//读操作
list.size();
}
},"Size线程").start();
}
}