Java多线程
一、如何创建一个java多线程程序?
1、创建一个多线程程序的方法
①继承Thread类(将任务和线程合并,也就是说一个线程一个任务,白话一点就是线程之间没有纽带关联,即没有共享资源)
②实现Runnable接口(将任务和线程分开,即所有线程的资源是共享的)
③实现Callable接口(利用线程池)
2、各方法简单实现
①继承Thread类
//1、创建一个继承于Thread类的子类
class TestNumber extends Thread {
//2、重写Thread类的run()方法,方法内实现此子线程要完成的功能
public void run() {
for(int i=0;i<=100;i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
//3、创建一个子类的对象
TestNumber tn1 = new TestNumber();
TestNumber tn2 = new TestNumber();
tn1.setName("子线程1: ");
tn2.setName("子线程2: ");
//4、调用线程的start()方法:启动此线程,调用相应的run()方法
tn1.start();
tn2.start();
}
}
②实现Runnable接口
//1、创建一个实现Runnable接口的类
class PrintNum implements Runnable{
//实现接口的抽象方法
@Override
public void run() {
// TODO 自动生成的方法存根
for(int i = 0 ; i<100 ; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class TestRunnable {
public static void main(String[] args) {
//3、创建一个Runnable接口实现类的对象
PrintNum p = new PrintNum();
//4、将此对象作为形参传递给Thread类的构造器中,创建Thread类
Thread t = new Thread(p);
t.start();
//再创建一个线程
Thread t2 = new Thread(p);
t2.start();
}
}
③实现Callable接口
//1、实现Callable接口,需要指定返回值类型
class MyCallable implements Callable<String> {
//重写call()方法,方法内实现子线程要完成的功能
@Override
public String call() throws Exception {
Thread.currentThread().setName("子线程: ");
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
return Thread.currentThread().getName() + "is succeed";
}
}
public class Callable_test {
public static void main(String[] args) {
//定义单线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//将自己定义的类放入线程池中,然后执行该线程
Future<String> future = threadPool.submit(new MyCallable());
Thread.currentThread().setName("主线程:");
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName() + i);
}
try {
//获取线程的返回值
System.out.println(future.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭线程池
threadPool.shutdown();
}
}
}
3、Thread、Runnable以及Callable的比较
①Thread的局限性
- 因为Thread是使用继承,而java不允许多继承,所以Thread具有单继承的局限性,即定义一个继承了Thread的类,一个实例只能执行一次,不能重复使用
- 各任务的成员的变量不共享,必须加static才能共享
②Runnable的优点以及局限性
Runnable的优点:
- 首先由于Runnable是使用接口实现的,所以没有多继承的局限,所以一个实例可以通过Runnable实例可以创建多个Thread实例就可以实现多线程
- 而这些线程的资源都是来自实现Runnable的类,所以变量是共享的
Runnable的局限性
- 任务没有返回值
- 任务无法抛出异常给调用方
③Callable优点
Callable接口类似于Runnable,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到
二、多线程控制
1、currentThread()
静态的,Thread.currentThread()可以获取当前线程的引用,一般都是在没有线程对象又需要获得线程信息时使用
而this也能获取道到当前线程的引用,但与currentThread()还是有差别的
this指代当前所在类的实例对象,currentThread是指代当前正在运行的线程
使用this会出现的问题
class CurrentThread extends Thread{
public void run() {
System.out.println("run方法开始");
System.out.println("this的线程名为: " + this.getName());
System.out.println("currentThread()的线程名为: " + Thread.currentThread().getName());
System.out.println("run方法结束");
}
}
public class Thread_currentThread {
public static void main(String[] args) {
CurrentThread mt = new CurrentThread();
Thread newthread = new Thread(mt);
newthread.setName("子线程");
newthread.start();
}
}
发现this和currentThread获取到的线程名不同
将线程对象以构造参数的方式传递给Thread对象进行start()启动线程,我们直接启动的线程实际是newthread,而作为构造参数的mt,赋给Thread类中的属性target,之后在newthread的run方法中调用target.run();
此时Thread.currentThread()是Thread的引用mt, 而this依旧是Thread的引用,所以是不一样的,打印的内容也不一样
2、run()与start()
线程的run()方法是由java虚拟机直接调用的,如果我们没有启动线程(没有调用线程的start()方法)而是在应用代码中直接调用run()方法,那么这个线程的run()方法其实运行在当前线程之中,而不是运行在其自身的线程中,从而违背了创建线程的初衷;
class TestThread extends Thread{
public void run() {
System.out.println("当前正在执行的线程名为: " + Thread.currentThread().getName());
}
}
public class Thread_Run_Start {
public static void main(String[] args) {
TestThread mt = new TestThread();
Thread.currentThread().setName("主线程");
mt.setName("子线程");
mt.run();
mt.start();
}
}
3、线程休眠:sleep
public static void sleep (long millis)
显示的让当前线程睡眠millis毫秒
class MyThread extends Thread{
public void run() {
for(int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i + ",时间:" + new Date());
//线程休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
public class ThreadSleep {
public static void main(String[] args) {
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
mt1.setName("大 雄");
mt2.setName("哆啦A梦");
mt1.start();
mt2.start();
}
}
没休眠前:
休眠后
4、加入线程:join()
public final void join()
在A线程中调用B线程的join()方法,表示,当执行到此方法时,A线程停止执行,直到B线程执行完毕,即先执行完调用join方法的线程
class joinThread extends Thread{
public void run() {
for(int i = 0; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
public class Threadjoin {
public static void main(String[] args) {
joinThread jt1 = new joinThread();
joinThread jt2 = new joinThread();
jt1.setName("先有鸡");
jt2.setName("后有蛋");
jt1.start();
//加入线程jt1
try {
jt1.join();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
jt2.start();
}
}
5、线程礼让:yield()
public static void yield()
调用此方法的线程主动释放当前的CPU执行权
class yieldThread extends Thread{
public void run() {
for(int i = 0; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
//线程礼让,当前线程主动放弃cpu,让其它线程执行
Thread.yield();
}
}
}
public class Threadyield {
public static void main(String[] args) {
yieldThread yt1 = new yieldThread();
yieldThread yt2 = new yieldThread();
yt1.setName("先有鸡");
yt2.setName("后有蛋");
yt1.start();
yt2.start();
}
}
6、后台线程(守护线程):setDaemon()
public final void setDaemon(boolean on):
将调用该方法的线程标记为守护线程或用户线程
当正在运行的线程都是守护线程时,java程序退出。该方法必须在启动线程前调用
class DaemonThread extends Thread{
public void run() {
for(int i = 0; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
public class ThreadSetDaemon {
public static void main(String[] args) {
DaemonThread dt1 = new DaemonThread();
DaemonThread dt2 = new DaemonThread();
dt1.setName("关羽");
dt2.setName("张飞");
Thread.currentThread().setName("刘备");
//将关羽、张飞都设置为守护线程
dt1.setDaemon(true);
dt2.setDaemon(true);
dt1.start();
dt2.start();
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
发现线程在被杀死的过程中还是能执行一下的
7、线程中断:stop()与interrupt()
stop()方法与interrupt()方法
stop()方法:
class StopThread extends Thread{
public void run() {
System.out.println("开始执行时间:" + new Date());
//线程休眠5秒钟
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
System.out.println("线程被终止了");
}
System.out.println("结束执行时间:" + new Date());
}
}
public class ThreadStop {
public static void main(String[] args) {
StopThread st = new StopThread();
st.start();
//超过3秒没有响应,就终止线程
try {
Thread.sleep(3000);
st.stop();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
stop是过时的方法,是直接暴力的停止线程,在上面的案例钟发现线程被终止了,但是线程后面的程序并没有执行,所以并不是很安全
interrupt()方法:
class InterruptThread extends Thread{
public void run() {
System.out.println("开始执行时间:" + new Date());
//线程休眠5秒钟
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
System.out.println("线程被终止了");
}
System.out.println("结束执行时间:" + new Date());
}
}
public class ThreadInterrupt {
public static void main(String[] args) {
InterruptThread it = new InterruptThread();
it.start();
//超过3秒没有响应,就终止线程
try {
Thread.sleep(3000);
it.interrupt();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
8、线程通信(生产者、消费者)
使用wait(),notify()以及notifyAll()
例子:
//资源池代码
public class publicResources {
private int Resources;
public publicResources(int begin) {
Resources = begin;
}
public publicResources() {
Resources = 0;
}
public synchronized void P() {
if (Resources >= 5){
//若资源数大于5个则休息
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
Resources++;
System.out.println("生产一个资源,当前还有" + Resources + "个资源");
//唤醒线程消费资源
this.notify();
}
public synchronized void V() {
//如果资源数小于0则等待资源
if (Resources <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//资源大于0,则消费资源
Resources--;
System.out.println("消费一个资源,当前还有" + Resources + "个资源");
//如果资源数等于0,则说明没有数据了,唤醒生产者
if (Resources == 0) {
this.notify();
}
}
}
//生产者代码
public class producer extends Thread {
private publicResources pr = null;
public producer(publicResources pr) {
this.pr = pr;
}
public void run() {
while (true) {
pr.P();
}
}
}
//消费者代码
public class consumer extends Thread {
private publicResources pr = null;
public consumer(publicResources pr) {
this.pr = pr;
}
public void run() {
while (true) {
pr.V();
}
}
}
//测试代码
public class Test {
public static void main(String[] args) {
publicResources pr = new publicResources(4);
producer p = new producer(pr);
consumer c = new consumer(pr);
p.start();
c.start();
}
}
结果:
9、线程状态转换图
三、锁
1、乐观锁——CAS
①概念
线程在读取数据时不加锁,但如果要对数据进行修改,那么在准备写回数据时,需要先去查看当前值是否被修改即当前值是否还是原来的值,若未被其它线程修改则写入修改,若已被修改则撤回修改操作,读取新的数据再进行操作。
②存在的问题
①ABA问题
-
线程1读取了数据A
-
线程2读取了数据A
-
线程2通过CAS⽐较,发现值是A没错,可以把数据A改成数据B
-
线程3读取了数据B
-
线程3通过CAS⽐较,发现数据是B没错,可以把数据B改成了数据A
-
线程1通过CAS⽐较,发现数据还是A没变,就写成了⾃⼰要改的值
在这个过程中所有线程都没做错什么但值被改变了,线程1却没有发现,但其实这个情况对最终结果并没有影响,因为要修改的值仍然是A
②循环时间长,开销大的问题
如果CAS长时间操作不成功的话,会导致一直自旋,相当于死循环,对CPU压力很大
③只能锁一个共享变量
对单个共享变量可以保证原子操作,但对于多个变量就无法保证了
③ABA问题解决方案
加标志位如使用一个自增的字段或使用时间戳表示这个值的版本
2、悲观锁——synchronized
①概念
同一时刻只有一个线程可以对临界区进行操作
即一个线程运行道这个方法的时候,要先检查有没有其它线程正在使用这个方法,有的话要等正在使用synchronized方法的线程运行完后再运行此线程,没有的话,锁定调用者,然后直接运行。
②如何使用synchronized
当一个线程使用了加了锁的对象/方法,实际上就是让加锁的对象/方法与一个对应的monitor相关联
Ⅰ.对对象进行加锁
如果线程进入,则得到当前对象的锁,那么别的线程在该类的所有对象上的任何操作都不能执行。使用的较少,对象级用锁是比较粗糙的用法,完全隔绝了其它线程访问此对象
而实际上如果一个对象拥有多个资源,就不需要为了让一个线程使用其中一部分资源就将所有线程都锁在对象外面。
Ⅱ.对函数(方法)进行加锁
可能会出现同一个线程同步调用的情况,这个时候方法所关联的monitor会维护一个拥有次数的计数器,当调用一次这个方法(执行了一次monitorenter)计数器加1,运行结束后计数器减1(执行一次minitorexit),当计数器为0时monitor将被释放,其它线程就可以调用该方法
Ⅲ.对代码块进行加锁
与对函数加锁类似
3、synchronized的优化
过去为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁
Ⅰ.偏向锁
在锁不存在多线程竞争的情况下,为了减小线程获取锁的代价而引入了偏向锁
三种情况:
- 如果本身是无锁状态(初始状态),将线程ID设置为当前线程ID,即偏向自己
- 如果当前对象是偏向锁,那么判断拥有该偏向锁的线程是否还存在,不存在的时候设置线程ID指向自己
- 如果拥有该偏向锁的线程还存在,那么说明同时有多个线程访问该对象即存在竞争,那么锁就会升级为轻量级锁
偏向锁适用于从始至终都只有一个线程在运行的情况
Ⅱ.轻量级锁
与CAS一样,当其它线程获得锁,当前线程就会尝试通过自旋来获得锁,若自旋10次还是未获得锁,说明线程执行的步骤比较复杂或者竞争锁的对象较多,那么就会将该锁升级为重量级锁,阻塞尝试获取锁的线程,当持有锁的线程释放锁之后会唤醒这些线程,然后再去争夺锁。
3、Volatile
①volatile两层含义:
1、内存可见,写操作先行于读操作
2、禁止指令重排序
②volatile与synchronized区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
③为什么要保证数据可见性
public class Test_Volatile {
public static void main(String[] args) {
Test t = new Test();
t.start();
while (true) {
if (t.getflag()) {
System.out.println("flag已经改为true了");
break;
}
}
}
}
class Test extends Thread {
private boolean flag = false;
public boolean getflag() {
return flag;
}
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
flag = true;
System.out.println("flag = " + flag);
}
}
你会发现,永远都不会输出 flag已经改为true了 这⼀段代码,按道理线程改了flag变量,主线程也能访问到的呀?
如果对flag添加Volatile修饰符或者锁就可以解决这个问题
private volatile boolean flag = false;
④数据不可见产生的原因
首先要解决的问题是,不加volatile之前,main函数明明调用了setGoal()方法,把goal改成了true,可为什么GoalNotifier线程里的goal还是false?
答案是,主线程里调用setGoal()方法修改的goal,和GoalNotifier线程里的goal,是两个副本。
为什么会是两个副本,不是同一个变量吗?
计算机,相比于处理器的运算速度,IO操作的速度往往有几个数量级的差距,因此像下面这段常见的++运算:
int count = 0;
count++;
如果计算机把count的值存储在内存中,那么每次++操作,就有一次从内存中读取i的值的操作,以及一次把i的值加1的操作,别忘了,还有一次把i的值写进去内存的操作:
T(一次循环) = T(读IO) + T(+1运算) + T(写IO)
而IO操作的速度往往比运算速度多几个数量级,所以:
T(一次循环) ≈ T(读IO) + T(写IO)
显然,IO操作的速度严重拖后腿了,不管运算速度再快,只要IO操作还在,这个++操作的速度就永远由IO操作的速度决定。
我们人类自然不允许这样的情况发生,因此我们在处理器和内存之间,引入了读写速度接近处理器运算速度的一层高速缓存:
在上面的++操作里面,count变量只有在初始化的时候,需要写入主内存,接着,count就被从主内存拷贝到处理器的高速缓存中,下次再想对它执行++操作时,直接从高速缓存中读取就可以了,++操作执行完之后,也不需要马上同步到主内存。
所以主线程中的变量是缓存中的flag而主线程并未对这个flag进行操作,自然flag的值一直是true
⑤为什么加了Volatile或Synchronized后数据就可见了呢?
Ⅰ.Volatle如何解决数据不可见问题
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
Ⅱ.Synchronized如何解决数据不可见问题
synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
4、公平锁与非公平锁
①非公平锁
A线程准备进去获取锁,若state状态是0,所以可以CAS成功,并且修改了当前持有锁的线程为自己。
B线程准备进去获取锁,若state状态是1,CAS失败,去等待队列,等待被唤醒。
若A线程执行完毕,会将state改未0,加锁线程改未null并唤醒等待队列中的队头的B线程。
若在A唤醒B线程的过程中C线程到了,发现state为0就会占有这个资源,B线程唤醒后发现state为0那么就会继续回到队列进行排队
②公平锁
与非公平锁的区别就是当碰到state为0时要先判断自己是否是队首
多线程购买电影票实战
class Windows implements Runnable {
int ticket = 10;
@Override
public void run() {
// Object obj = new Object();//局部变量,如果作为锁的话,不唯一!
// TODO 自动生成的方法存根
while (ticket > 0) {
show();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
public synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
}
}
}
public class 购买电影票 {
public static void main(String[] args) {
Windows w = new Windows();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
也可以将方法改为代码块
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
} else {
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}