多线程的优点
多线程就是为了充分利用CPU的资源,可能会 提高程序执行效率。这里强调的是可能,使用多线程并不一定会提高程序的执行效率。
创建多线程的三种方式
-
继承Thread
优点:可以直接使用Thread类中的方法。代码简单
缺点:继承Thread类之后就不能继承其他类 -
实现Runnable接口
优点:即使自定义类已经有了父类也不受影响,因为可以实现多个接口
缺点:在run方法内部需要获取到当前线程的Thread对象后才能使用Thread中的方法 -
实现Callable接口
优点:可以获取返回值,可以抛出异常
缺点:代码编写较为复杂
Thread中常用的方法
start()
启动线程,该方法调用后,线程不会立即执行,当JVM调用run方法时才会真正执行。
setName()
设置线程的名字rue
getName()
获取线程的名字
currentThread()
获取当前线程的对象,在使用实现Runnable接口去创建线程的时候,就可以使用该方法获取线程的对象了。
setPriority(int )
设置线程的优先级,1-10之间的整数,数字越大优先级越高。
sleep(long millis)
使当前线程睡眠,线程睡眠后,里面的任务不会执行,待睡眠时间过后会自动苏醒,从而继续执行任务。
interrupr()
如果线程处于正常活动状态,那么会将该线程的中断标志设置为t;唤醒正在睡眠的线程
yield()
当前线程在执行该方法后会进行礼让,即本来CPU执行A线程,但在A线程中调用了yield()方法,此时CPU会放弃A线程的执行权,但是放弃的时间不确定,有可能刚放弃,A线程马上又获得了CPU的执行权。
join()
线程加入,可以理解为两个线程的合并,有两个线程A和B,A线程需要等B线程执行完毕后再执行,此时就可以使用join方法。
Java多线程的关键字
synchronized :可以同步方法,也可以同步代码块
volatile:保证多个线程之间变量的可见性。
并行和并发
并行就是两个或两个以上的任务同时运行,就是A任务进行的同时,B任务也在进行。(需要多核CPU)
并发是指两个或两个以上的任务都请求运行,而CPU只能接受一个任务,就把这两个任务安排轮流进行,由于时间 间隔较短,使人感觉两个任务都在运行。
多线程创建的三种方式之继承Thread类
在java里面,开发者可以创建线程,这样在程序执行过程中,如果CPU空闲了,就会执行线程中的内容。
package exe;
/*
* 1.自定义一个类,继承java.lang包下的Thread类
2.重写run方法
3.将要在线程中执行的代码编写在run方法中
4.创建上面自定义类的对象
5.调用start方法启动线程
*/
public class Test01 {
public static void main(String[] args) {
//4.创建上面自定义类的对象
thread1 t = new thread1();
//5、5.调用start方法启动线程
t.start();
for(int i =0;i<1000;i++) {
System.out.println("main线程中的代码");
}
}
}
//1.自定义一个类,继承java.lang包下的Thread类
class thread1 extends Thread{
//2.重写run方法
public void run() {
//3.将要在线程中执行的代码编写在run方法中
for(int i =0;i<1000;i++) {
System.out.println("使用继承Thread方法创建的线程");
}
}
}
多线程创建之实现Runnable接口
ackage exe;
/*
* 多线程创建的三种方式之实现Runnable接口
* 1.自定义一个类实现java.lang包下的Runnable接口
2.重写run方法
3.将要在线程中执行的代码编写在run方法中
4.创建上面自定义类的对象
5.创建Thread对象并将上面自定义类的对象作为参数传递给Thread的构造方法
6.调用start方法启动线程
*/
public class Test02 {
public static void main(String[] args) {
//4.创建上面自定义类的对象
thread2 t2 = new thread2();
//5.创建Thread对象并将上面自定义类的对象作为参数传递给Thread的构造方法
Thread thread = new Thread(t2);
//6.调用start方法启动线程
thread.start();
for(int i =0;i<1000;i++) {
System.out.println("main主线程");
}
}
}
//1.自定义一个类实现java.lang包下的Runnable接口
class thread2 implements Runnable{
//2.重写run方法
@Override
public void run() { //3.将要在线程中执行的代码编写在run方法中
for(int i =0;i<1000;i++) {
System.out.println("使用runnable接口实现的线程");
}
}
多线程创建的三种方式之实现Callable接口
与其它两种方式不同的地方在于使用Callable接口创建线程会获得一个返回值并且可以声明异常。
package exe;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/*
* 多线程创建的三种方式之实现Callable接口
* 1.自定义一个类实现java.util.concurrent包下的Callable接口
2.重写call方法
3.将要在线程中执行的代码编写在call方法中
4.创建ExecutorService线程池
5.将自定义类的对象放入线程池里面
6.获取线程的返回结果
7.关闭线程池,不再接收新的线程,未执行完的线程不会被关闭
*/
public class Test03 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//4.创建ExecutorService线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//5.将自定义类的对象放入线程池里面
Future<Integer> f1 = es.submit(new thread3(5));
Future<Integer> f2 = es.submit(new thread3(3));
//6.获取线程的返回结果
// System.out.println(f1.get());
// System.out.println(f2.get());
//判断线程中的任务是否执行完毕
//判断线程中的任务是否执行完毕
if(f1.isDone()) {
System.out.println(f1.get());
}else {
System.out.println("f1线程中的任务还未执行完毕");
}
if(f2.isDone()) {
System.out.println(f2.get());
}else {
System.out.println("f2线程中的任务还未执行完毕");
}
//7.关闭线程池,不再接收新的线程,未执行完的线程不会被关闭
es.shutdown();
System.out.println("main方法结束");
}
}
// * 1.自定义一个类实现java.util.concurrent包下的Callable接口
class thread3 implements Callable<Integer>{
private int count;
public thread3(int count) {
super();
this.count = count;
}
//2.重写call方法
@Override
public Integer call() throws Exception { //3.将要在线程中执行的代码编写在call方法中
int sum = 1;
if(count==0) {
sum = 0;
}else {
for(int i =1;i<=count;i++) {
sum = sum*i;
}
}
//打印线程名称
System.out.println(Thread.currentThread().getName());
return sum;
}
}
线程池
线程池是初始化一个多线程应用程序过程中创建一个 线程集合,然后在 需要执行 新的任务时直接 去这个线程
集合中获取,而不是创建一个线程。任务执行结束后,线程回到池子中等待下一次的分配。
线程池的作用
解决创建单个线程耗费时间和资源的问题
使用匿名内部类创建线程
package exe;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/*
* 匿名内部类创建多线程
*/
public class Test04 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
/*
* Thread
*/
new Thread() { //1、继承Thread类
public void run() {//2、重写run方法
for(int i=0;i<1000;i++) {//3、将要执行的代码写在run方法中
System.out.println("Thread");
}
}
}.start();//4、开启线程
/*
* Runnable
*/
new Thread(new Runnable() { //1、将Runnable的子类对象传递给Thread的构造方法
public void run() { //2、重写run方法
for(int i =0;i<1000;i++) { //3、将要执行的代码写在fun方法中
System.out.println("runnavle");
}
}
}).start();//开启线程
/*
* Callable
*/
ExecutorService exec = Executors.newCachedThreadPool(); //1、创建线程
Future<Integer> result = exec.submit(new Callable<Integer>() { //2、向线程池中添加线程
@Override
public Integer call() throws Exception { //3、重写call方法
// TODO Auto-generated method stub
return 1024;
}
});
System.out.println(result.get()); //4、获取返回结果
}
}
设置线程的名字
通过Thread对象的setName() 方法给线程设置名字
通过Thread对象的getName()方法获取线程的名字
获取线程对象
在使用Runnable接口来创建线程的时候,run方法无法使用Thread类中的getName()方法,这时可以使用Thread.currentThread()方法来获取Thread的对象,通过对象调用 getName()方法。
线程睡眠
Thread中的sleep方法可以使当前线程睡眠,线程睡眠后,里面的任务不会执行,待睡眠时间过后会自动苏醒,从而继续执行任务。
Thread中有两个重载的sleep方法
sleep(long millis),指定睡眠毫秒数
sleep(long millis,int nanos),第一个参数是毫秒,第二个参数是纳秒
设置线程的优先级
可以通过使用Thread类中的setPriority方法设置线程的优先级。
setPriority()方法接收一个int类型的参数通过这个参数可以指定线程的优先级,取整范围是整数 1~10,优先级随着数字的增大而增强。
唤醒正在睡眠的线程
可以使用Thread类中的 interrupt方法唤醒正在睡眠的线程,调用interrupt方法会抛出一个interruptedException的异常。
Synchronized同步方法
成员变量存储在堆内存里面,多个线程访问同一个堆内存,即多个线程可以 同时修改num的值,这样会导致线程安全问题。
同步和异步
比如你要给A,B,C三人发消息
同步:先给A发,等A回复后,再给B发,等B回复后,再给C发,排队等待
异步:直接给A,B,C发消息,中间不需要等待某人回复之后再给其它人发消息,不用排队等待。
使用Synchronized将方法设置为同步
public synchronized void changeNum(boolean flag)
在方法上加入synchronized关键字,这样在执行多个线程时看哪个线程先执行这个方法,假设有 t1,t2,t3三个线程中都调用了changeNum方法,t1线程先执行了这个方法,那么t1会先在Task对象上面加锁,加锁后,别的线程就无法执行当前Task对象上的changeNum方法,直到t1执行结束changeNum方法之后,t2,t3中的一个线程才可以执行这个方法,这就保证了在某个时间段内只有一个线程执行changeNum方法,解决了线程安全问题。
注意:Synchronized锁住的是当前对象,如果t1线程和t2线程里面是不同的对象,则不需要同步,因为不会发生线程安全问题。
package exe;
/*
Synchronized同步方法
线程安全问题: 定义一个Task类,里面有一个成员变量和一个boolean类型参数的方法,方法内部会根据传入
参数修改 成员变量的值
*/
class Task{
private int num = 0;
/*在方法上加入 synchronized关键字,这样在执行多个线程时看哪个线程 先执行这个方法,假设有 t1,t2,t3三个线程 中都调用了
*
* changeNum方法,t1线程 先执行了这个方法,那么t1会先在Task对象上面枷锁,加锁后,别的线程 就无法执行当前Task对象的 changeNum方法,
*
* 直到 t1执行结束 changeNum方法之后,t2,t3中的一个线程才可以执行这个方法,这就保证了在某个时间段内只有一个线程 执行 changeNum方法,解决了线程安全问题
*
* 注意:Synchronized锁住的是当前对象,如果 t1线程和t2线程里面是不同的对象,则不需要同步。
*/
public synchronized void changeNum(boolean flag) {
if(flag) {
num = 88;
System.out.println(Thread.currentThread().getName()+"========"+"begin");
System.out.println(num);
System.out.println(Thread.currentThread().getName()+"======="+"end");
}else {
num = 66;
System.out.println(Thread.currentThread().getName()+"========"+"begin");
System.out.println(num);
System.out.println(Thread.currentThread().getName()+"======="+"end");
}
}
}
public class Test10 {
//成员变量存储在堆内存里面,多个线程 访问 同一个 堆内存
//即多个线程 可以同时修改 num的值,这样会导致线程安全问题
public static void main(String[] args) {
//创建一个Task对象,将这个对象放到两个线程中,在这两个线程中分别调用 changeNum() 方法
Task task = new Task();
Thread t1 = new Thread() {
public void run() {
task.changeNum(true);
}
};
Thread t2 = new Thread() {
public void run() {
task.changeNum(false);
}
};
t1.start();
t2.start();
}
}
Synchronized同步代码块
package exe;
/*
* Synchronized同步代码块
*
* Synchronized同步方法的问题:
* 有些情况下,在方法上面加synchronized同步,会有性能问题
*
*/
class LongTask{
private int num = 0;
Object obj = new Object();
/*
*
*/
//同步和异步
public void changeNum(boolean flag) {
//假设下面代码执行一个耗时较长的任务,并且这段任务不涉及到线程安全问题
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//这个方法中,需要同步的代码块是这部分,而上面耗时操作的代码,不涉及到线程安全问题,所以不需要同步
synchronized(obj) {
if(flag) {
num = 88;
System.out.println(Thread.currentThread().getName()+"========"+"begin");
System.out.println(num);
System.out.println(Thread.currentThread().getName()+"======="+"end");
}else {
num = 66;
System.out.println(Thread.currentThread().getName()+"========"+"begin");
System.out.println(num);
System.out.println(Thread.currentThread().getName()+"======="+"end");
}
}
}
}
public class Test11 {
/*
将一个耗时较长的任务放到两个线程中,计算这两个线程执行结束后所花费的时间。
*/
public static long begin1;
public static long end1;
public static long begin2;
public static long end2;
public static void main(String[] args) {
final LongTask longTask = new LongTask();
Thread t1 = new Thread() {
public void run() {
begin1 = System.currentTimeMillis();
//执行耗时较长的任务方法
longTask.changeNum(false);
end1 = System.currentTimeMillis();
}
};
Thread t2 = new Thread() {
public void run() {
begin2 = System.currentTimeMillis();
//执行耗时任务较长方法
longTask.changeNum(true);
end2 = System.currentTimeMillis();
}
};
t1.start();
t2.start();
//先让主线程睡眠,保证t1和t2线程执行完毕之后再计算时间
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
long begin = 0;
long end = 0;
//将先执行的贤臣的时间和最后执行线程的时间获取到
if(begin1>begin2) {
begin = begin2;
}else {
begin = begin1;
}
if(end1>end2) {
end = end1;
}else {
end = end2;
}
System.out.println("两个线程总耗时:"+(end-begin)/1000+"秒"); //输出总耗时:6秒
}
}
注意:多个线程在执行synchronized同步代码块时,代码括号里面可以传入任意对象,但一定要保证多个线程访问的是同一个对象。
单例模式
单例模式程序运行时,让某个类在内存中只有一个对象,即让一个类只能创建一个对象。
一般分为懒汉式和饿汉式。
package exe;
/*
单例模式:
单例模式: 程序运行时,让某个类 在内存中只有 一个对象,即让一个类 只能创建一个对象
一般分为懒汉式和饿汉式
*/
public class Test15 {
/*
* 饿汉式:
* ①构造方法私有化
* ②创建当前类对象
* ③对外提供公共的访问方法 将SingletonHungary对象暴露给外部
*/
/*
//1、构造方法私有化
private Test15() {
}
//2、创建当前类对象
private static Test15 s = new Test15();
//3、对外提供公共的访问方法将 SingletonHungary对象暴露给外部
public static Test15 getInstance() {
return s;
}
*/
/*
懒汉式
①构造方法私有化
②创建当前类的引用
③对外提供公共的访问方法将SingletonHungary对象暴露给外部
*/
//1构造方法私有化
private Test15() {
}
//2、创建当前类的引用
private static volatile Test15 s;
//3、对外提供公共的访问方法将 SingletonHungary对象暴露给外部
public static Test15 getInstance() {
if(s==null) {
//这里有可能会有两个线程进来,所以还需要在下面再次判断s是否为null,该方式被称为双重检查
synchronized (Test15.class) {
if(s==null) {
s = new Test15();
}
}
}
return s;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
单例模式的案例Runtime
java.lang包下的 Runtime类使用了单例模式,使用该类可以执行 windows系统里面的 一些命令,例如:
mspaint(打开画图软件),shutdown(关机)等等
使用Timer类来实现定时任务
定时任务就是让计算机自动的每隔一段时间执行的代码。
比如要实现这样的一个功能:
让计算机每隔5秒钟,在控制台打印 一段字符串
可以使用 java.util包下的 Timer类 和 TimerTask 类来实现。
TimerTask是一个实现了Runnable接口的抽象类,需要编写 一个类继承 TimerTask类,将要在定时任务时执行的代码编写在run方法中。
package com.reflect;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;
public class exe01 {
public static void main(String[] args) throws ParseException {
Timer t = new Timer(); //查看API
t.schedule(new TimerTask() { //匿名构造类
@Override
public void run() {
System.out.println("Hello,ZhouJian!");
}
}, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2019-03-07 18:40:00"), 5000);
}
}
线程之间的通信
两个线程之间的通信
多线程环境下CPU会随机的在线程之间进行切换,如果想让两个线程有规律的去执行,那就需要两个线程之间进行通信,在Object类中的两个方法wait和notify可以实现通信。
wait方法可以使当前线程进入等待状态,在没有被唤醒的情况下,线程会一直保持等待状态。notify方法可以随机唤醒单个在等待状态下的线程。
使用互斥锁唤醒指定线程
线程的生命周期
线程的生命周期主要分为下面5个转态:
- 新建
- 就绪
- 运行
- 阻塞
- 死亡