一、线程与进程
进程:系统分配资源的最小单位。
单进程程序 -》多进程程序(缺点:不能共享资源),故此有了线程。
线程:是系统调度的最小单位。(CPU的调度)。轻量级“进程”。
优点:创建的时候占用更少的资源,并且多个线程之间可以共享资源。
线程共享的资源:1、打开的文件
2、共享内存(new object)
线程与进程的区别:1、进程是系统分配资源的最小单位,线程是系统调度的最小单位
2、一个进程中至少包含一个线程。
3.线程必须要依附于进程,线程是进程实质工作的一个最小单位。
二、线程可以做什么?
1.使用线程休眠来实现电脑字幕打印的功能。
public static void main(String[] args) throws InterruptedException {
String content = "每个人都会经过一个阶段,见到一座山,就想知道山后面是什么。我很想告诉他,可能翻过山后面,你会发现没什么特别。回望之下,可能会觉得这一边更好。";
for (char item:content.toCharArray()) {
System.out.print(item);
//休眠300ms
Thread.sleep(100);
}
}
2.单线程和多线程的程序对比
public class ThreadDemo2 {
private static final long count = 5_0000_0000L;
public static void main(String[] args) throws InterruptedException {
//多线程方法
multithreading();
//单线程方法
singlethread();
}
//单线程
private static void singlethread() {
long stime = System.currentTimeMillis();
int a = 0;
for (int i = 0; i < 3*count; i++) {
a++;
}
long etime = System.currentTimeMillis();
System.out.println("单线程执行的时间:"+(etime-stime));
}
//多线程
private static void multithreading() throws InterruptedException {
long stime = System.currentTimeMillis();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (int i = 0; i < count; i++) {
a++;
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
int b = 0;
for (int i = 0; i < count; i++) {
b++;
}
}
});
t2.start();
int c = 0;
for (int i = 0; i < count; i++) {
c++;
}
//等待t1,t2执行完
t1.join();
t2.join();
long etime = System.currentTimeMillis();
System.out.println("多线程执行的时间:"+(etime-stime));
}
}
三、线程的创建方式
1.继承Thread类
方法一:
public class ThreadDemo3 {
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
System.out.println("线程的名称(主线程):"+Thread.currentThread().getName());
}
}
方法二:
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
};
thread.start();
}
继承Thread类的方式的缺点:在java语言当中只能实现单继承,如果继承了Thread类,也就不能继承其他类。
2.实现Runnable接口的方式
方法一:
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
方法二(主流):
public static void main(String[] args) {
//匿名内部类的方式实现线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
});
thread.start();
}
方法三(jdk1.8主流):
public static void main(String[] args) {
//lambda+匿名runnable的实现方式
Thread thread = new Thread(() -> {
System.out.println("线程名:"+Thread.currentThread().getName());
});
thread.start();
}
3.实现Callable接口的方式:可以得到线程执行之后的结果
//创建线程的任务和实现方法
static class MyCallable implements Callable{
@Override
public Object call() throws Exception {
int num = new Random().nextInt(10);
System.out.println("子线程:"+Thread.currentThread().getName()
+"随机数:"+num);
return num;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
int result = futureTask.get();
System.out.println(String.format("线程名: %s, 结果: %d"
,Thread.currentThread().getName(),result));
}
四、线程休眠
方法一:(精度小,时间短)
方法二:(时间长)
方法三:
五、面试题:使用两个线程打印"AABBCCDD"
public static void main(String[] args) {
String str = "ABCD";
Runnable runnable = new Runnable() {
@Override
public void run() {
for (char item:str.toCharArray()){
System.out.print(item);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
六、Thread常见的构造方法
1.创建线程对象
2.使用Runnable对象创建线程对象
3.使用Runnable对象创建线程对象,并命名
4.线程分组:可以将一类线程归为一组,并且进行线程的打印,查看一组线程的具体行为。
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("开始起跑:"
+Thread.currentThread().getName());
int num = new Random().nextInt(5)+1;
try {
Thread.sleep(num*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("到终点了:"
+Thread.currentThread().getName());
}
};
//定义分组
ThreadGroup group = new ThreadGroup("百米赛跑一组");
//创建运动员
Thread t1 = new Thread(group,runnable,"张三");
Thread t2 = new Thread(group,runnable,"李四");
t1.start();
t2.start();
group.list();
//等待选手到达终点 ==0 说明以跑完
while (group.activeCount() != 0) {
}
System.out.println("宣布成绩");
}
七、Thread常见的属性
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
},"张三");
System.out.println("线程状态 "+t1.getState());
t1.start();
System.out.println("线程状态II "+t1.getState());
System.out.println("线程ID "+t1.getId());
System.out.println("线程名称 "+t1.getName());
System.out.println("线程优先级 "+t1.getPriority());
System.out.println("线程是否为后台线程 "+t1.isDaemon());
System.out.println("线程是否存活 "+t1.isAlive());
System.out.println("线程是否被中断 "+t1.isInterrupted());
}
1.线程优先级
优先级越高,执行的优先级也越高,执行权也就越大,但是CPU的调度很复杂,也不会严格的按照优先级的排序去执行,但总体来看,还是优先级越高执行的权重也就越高。
2.存活状态
仅仅只是创建了线程类MyThread对象,但是并未启动线程,所以线程处于未活动状态,是false;
方法isAlive()的功能是判断当前的线程是否处于活动状态。什么是活动状态呢?
活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
3.后台线程
线程的分类:(1)后台线程(守护线程):true
(2)用户线程(默认):false
守护线程是用来服务用户线程的
进程退出:没有用户线程运行,进程就会结束。
守护线程使用场景:java垃圾回收器
守护线程注意事项:(1)守护线程的设置必须在调用start()之前。如果设置守护线程在start()之后,则程序会报错,并且设置的守护线程不
能生效。
(2)在守护线程里面创建的线程,默认情况下都是守护线程。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 在守护线程的内部创建的线程
Thread t2 = new Thread(() -> {
});
System.out.println("t2 是:" +
(t2.isDaemon() == true ? "守护线程" : "用户线程"));
});
// 设置为守护线程
/*t1.setDaemon(true);*/
t1.start();
System.out.println("t1 是否为守护线程:" + t1.isDaemon());
}
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 在守护线程的内部创建的线程
Thread t2 = new Thread(() -> {
});
System.out.println("t2 是:" +
(t2.isDaemon() == true ? "守护线程" : "用户线程"));
});
// 设置为守护线程
t1.setDaemon(true);
t1.start();
System.out.println("t1 是否为守护线程:" + t1.isDaemon());
}
4.线程状态
(1)打印所有的线程状态
八、线程的方法
1.start()和run()方法的区别
(1)run属于普通方法,而start方法属于启动线程的方法。(调用start方法启动的是新创建的线程,而调用run方法启动的是main)
(2)run方法可以执行多次,而start方法只能执行一次。
2.线程中断
(1)使用全局自定义的变量来终止线程。(在拿到终止指令之后,需要执行完当前的任务才会终止)
//定义全局自定义变量
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
//转帐线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在转帐");
}
System.out.println("转帐终止");
}
});
t1.start();
//终止转帐线程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(310);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("停止转账,有内鬼");
flag = true;
}
});
t2.start();
t1.join();
t2.join();
}
(2)使用线程提供的方法interrupt来终止线程。(会在收到终止指令之后,会立马结束执行)作用:将线程中的终止状态从默认的false改为true.
public static void main(String[] args) throws InterruptedException {
//转账线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.interrupted()) {//判断线程的终止状态
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
System.out.println("正在转帐");
}
System.out.println("转帐终止");
}
});
t1.start();
Thread.sleep(310);
System.out.println("有内鬼,终止转帐");
t1.interrupt();//终止线程
}
public static void main(String[] args) throws InterruptedException {
//转账线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//while (!Thread.currentThread().isInterrupted()) //拿到了当前线程的interrupt状态,不会进行复位
while (!Thread.interrupted()) {//判断线程的终止状态 //拿到全局的interrupt状态,用过一次之后会自动复位(恢复原来的状态)
System.out.println("正在转帐");
}
System.out.println("转帐终止");
}
});
t1.start();
Thread.sleep(10);
System.out.println("有内鬼,终止转帐");
t1.interrupt();//终止线程
}
3.等待一个线程
thread.join();
4.获取当前线程的引用
5.休眠当前的线程
Thread.sleep(long millis);
Thread.sleep(long millis,int nanos);//精度到纳秒
6.yield()
出让CPU执行权,特点:不一定能够正常出让CPU的执行权。
九、多线程带来的风险-线程安全问题
1.线程不安全:多线程执行中,程序的执行结果和预期不相符。
2.线程不安全的原因:
(1)CPU抢占式执行。(万恶之源)
(2)原子性
(3)编译器优化(代码优化、指令重排序):编译器优化在单线程下没问题,可以提升程序的执行效率,但是在多线程下就会出现混乱(编译器优化的本质是修改代码的执行顺序)从而导致线程不安全的问题。
(4)(内存)可见性
(5)多个线程修改了同一个变量
3.volatile:轻量级解决“线程安全”的方案
volatile的作用:
(1)禁止指令重排序
(2)解决线程可见性的问题:当操作完变量之后,强制删除掉线程工作内存中的此变量。
注意事项:volatile不能解决原子性的问题
(3)在关键代码让CPU排队执行,加锁。
锁操作的关键步骤:
(1)尝试获取锁(如果成功拿到锁则加锁,否则排队等待)
(2)释放锁
4.java中解决线程安全问题的方案
(1)锁(让多线程排队执行)
(2)使用私有变量。
5.锁的解决方案
(1)synchronized加锁和释放锁【JVM层面的解决方案,自动帮我们进行加锁和释放锁】
private static int num = 0;
private static final int MAXSIZE = 100000;
public static void main(String[] args) throws InterruptedException {
//创建一把锁
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
//实现加锁
synchronized (lock) {
num++;
}
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
//实现加锁
synchronized (lock) {
num--;
}
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println(num);
}
注意事项:在加锁操作时,同一组业务一定是同一组锁对象。
synchronized把锁存在了lock的对象头里面
对象头信息:
互斥锁结构体mutex:
synchronized实现原理:1)从操作系统层面上来说:互斥锁mutex
2)JVM:帮我们实现的监控器锁的加锁和释放锁操作。
3)JAVA:a)锁对象
b)锁存放的地方:变量的对象头
JDK 6 对synchronized进行优化(锁升级):
(2)Lock手动锁【程序员自己进行加锁和释放锁】
private static int num = 0;
private static final int MAXSIZE = 100000;
public static void main(String[] args) throws InterruptedException {
//1,创建手动锁
Lock lock = new ReentrantLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
//2.加锁
lock.lock();
try {
num++;
} finally {
//3.释放锁
lock.unlock();
}
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
//2.加锁
lock.lock();
try {
num--;
} finally {
//3.释放锁
lock.unlock();
}
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println(num);
}
Lock的使用:一定要把lock()放在try外面(1.如果将lock()方法放在try里面,那么就当try里面的代码出现异常之后,就会执行finally里面释放锁的代码,但这个时候加锁还未成功。2.执行finally里面释放锁的代码就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本)
6.锁
(1)synchronized和Lock的区别
synchronized的锁机制时非公平锁
公平锁可以按顺序执行,而非公平锁执行的效率更高(抢占式)。
在java中所有锁默认的策略都是非公平锁。
Lock默认的所策略也是非公平锁,但是Lock可以显示的声明为公平锁。
(2)公平锁实现打印“AABBCCDD”
public static void main(String[] args) throws InterruptedException {
//声明一个公平锁
Lock lock = new ReentrantLock(true);
Runnable runnable = new Runnable() {
@Override
public void run() {
for (char item:"ABCD".toCharArray()) {
lock.lock();
try{
System.out.print(item);
} finally {
lock.unlock();
}
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread.sleep(10);
t1.start();
t2.start();
}
(3)synchronized的使用场景:
a)使用synchronized来修饰代码块(加锁对象可以自定义)
b)使用synchronized来修饰静态方法(加锁对象时当前类的对象)
c)使用synchronized来修饰普通方法(加锁对象时当前类的实例)
而Lock只能修饰代码块
十、面试题:volatile和synchronized的区别
volatile可以解决内存可见性问题和禁止指令重排序,但volatile不能解决原子性问题;synchronized是用来保证线程的安全,也就是synchronized可以解决而温暖和关于线程安全的问题(关键代码排队执行,始终只有一个线程会执行加锁操作)。
十一、面试题:synchronized和Lock之间的区别?
a)synchronized既可以修饰代码块,又可以修饰静态方法或者普通方法;而Lock只能修饰代码块。
b)synchronized只有非公平锁的策略,而Lock既可以是公平锁又可以是非公平锁。
c)ReentrantLock更加的灵活(比如tryLock获取锁失败可以二次执行).
d)synchronized是自动加锁和释放锁的,而ReentrantLock需要自己加锁和释放锁