1.多线程的创建方式
- Thread创建
- Runnable创建
- Lambda表达式创建
- Callable和 Future创建线程
- 使用线程池创建
package com.zw.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
public class CreateThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
new ThreadClass().start(); // 1.Thread创建
new Thread(new RunnableClass()).start(); // 2.Runnable创建
new Thread(() -> { // 3.Lambda表达式创建
System.out.println("Lambda is run...");
}).start();;
// 4.Callable和 Future创建线程
// Runnable的缺点:
// 1. run没有返回值
// 2. 不能抛异常
CallableThread callableThread = new CallableThread();
FutureTask<String> task = new FutureTask<String>(callableThread);
new Thread(task).start();
// 获取返回结果
String result = task.get();
System.out.println("Callable's result is " + result);
// 5.使用线程池创建
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程池创建...");
}
});
}
}
class ThreadClass extends Thread {
@Override
public void run() {
System.out.println("ThreadClass is run...");
}
}
class RunnableClass implements Runnable {
@Override
public void run() {
System.out.println("RunnableClass is run...");
}
}
class CallableThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("CallableThread is run...");
return "success";
}
}
ThreadClass is run...
RunnableClass is run...
Lambda is run...
CallableThread is run...
Callable's result is success
线程池创建...
2.Thread中的常用的方法
package com.zw.thread;
public class ThreadMethods {
public static void main(String[] args) throws InterruptedException {
Thread test1 = new Thread(() -> {
// 1.获得当前线程 Thread.currentThread()
System.out.println(Thread.currentThread().getName()); // test1 getName()返回线程名称
// 3.让当前线程休眠指定的毫秒数
try {
Thread.sleep(1000); // 休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 4.放弃当前的cpu资源 转为就绪状态
System.out.println("放弃当前的cpu资源");
Thread.yield();
try {
Thread.sleep(1000); // 休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("重新获取cpu资源"); // 重新获取cpu资源
}, "test1");
// 2.isAlive() 判断当前线程是否处于活动状态
System.out.println(test1.isAlive()); // false
test1.start();
System.out.println(test1.isAlive()); // true
Thread.sleep(1000);
// 5.放弃当前的cpu资源,主线程拿到资源
System.out.println("主线程拿到资源"); // 主线程拿到资源
// 6.setPriority() 设置线程的优先级别,优先级别越高越容易,不能保证优先级高的线程先运行
// 7.interrupt() 中断线程 我们可以interrupted()判断来手动停止 比如break return
System.out.println(test1.interrupted()); // false
// test1.interrupt(); // 会报错 因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。
// 8.守护线程 线程分为用户线程与守护线程
// 守护线程是为其他线程提供服务的线程,如垃圾回收(GC)就是一个典型的守护线程。
// 守护线程不能单独运行,当jvm中没有其他用户线程,只有守护线程时,守护线程会自动销毁,jvm会自动退出。
Thread test2 = new Thread(() -> {
System.out.println("守护线程开启");
}, "test1");
test2.setDaemon(true);
test2.start();
}
}
3.多线程的状态
- 新建状态
- 就绪状态,拿到cpu资源就可以执行
- 运行状态,拿到了cpu资源开始执行程序
- 阻塞状态,处于运行状态的线程因为某种原因放弃cpu资源进入阻塞状态
- 终止状态,运行完了自动进入
阻塞状态分为三种:
- 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
- 同步阻塞:运行的线程访问的资源被加了锁,JVM会把该线程放入“锁池”中
- 其它阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
4.线程安全问题
多线程虽然提高了程序的效率,但是当多个线程对同一个资源进行访问的时候会带来线程同步的问题,
主要有以下的问题。
4.1原子性
不可分割的操作单位,对于一个资源的改变要么全部执行,要么全部不执行。
int i = 0; // 原子操作
int j = i ++; //不是原子操作 因为分为两步 i = i + 1, j = i
保持原子性我们可以有两种方法:
- 加锁,保证同一时刻只有一个线程访问
- CAS指令 :直接在硬件层次上实现,看做是一个硬件锁。
4.2可见性
对一个共享变量进行修改后其它的线程没能够及时获取到更新的值。
4.3有序性
在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
编译器可能会改变两个程序的先后顺序;
处理器也可能不会按照目标代码的顺序执行;
5.解决多线程安全问题 线程同步
案例:
package com.zw.thread;
public class ThreadQuestion {
public static void main(String[] args) {
ThreadRunnable tRunnable = new ThreadRunnable();
new Thread(tRunnable, "线程1").start();
new Thread(tRunnable, "线程2").start();
}
}
class ThreadRunnable implements Runnable {
private int i = 100;
@Override
public void run() {
while (true) {
if (i <= -1) break;
i --;
System.out.println(Thread.currentThread().getName() + ": i = " + i);
}
}
}
线程2: i = 5
线程2: i = 4
线程2: i = 3
线程2: i = 2
线程2: i = 1
线程2: i = 0
线程2: i = -1
线程1: i = 6
当多个线程访问统一资源,出现了线程安全的问题。为了使得线程同步,有以下方法解决。
5.1 加锁
锁的作用: 锁可以实现对共享数据的安全访问,保障线程的原子性,可见性,与有序性。
5.1.1 synchronized
内部锁:synchronized可以加在代码块或者方法上。这种锁是一种排他锁
package com.zw.thread;
public class ThreadQuestion {
public static void main(String[] args) {
ThreadRunnable tRunnable = new ThreadRunnable();
new Thread(tRunnable, "线程1").start();
new Thread(tRunnable, "线程2").start();
}
}
class ThreadRunnable implements Runnable {
private int i = 100;
@Override
public void run() {
synchronized (this) {
while (true) {
if (i <= 0) break;
i --;
if (i == 66) {
System.out.println(Thread.currentThread().getName() + ": i = " + i);
break;
}
System.out.println(Thread.currentThread().getName() + ": i = " + i);
}
}
}
}
线程2: i = 9
线程2: i = 8
线程2: i = 7
线程2: i = 6
线程2: i = 5
线程2: i = 4
线程2: i = 3
线程2: i = 2
线程2: i = 1
线程2: i = 0
synchronized无论是正常结束还是抛出异常后,都会释放锁,而lock必须手动释放锁才可以。
5.1.2 显示锁Lock
和synchronized的区别:
- 如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。
- synchronized无法知道线程有没有成功获取到锁
公平锁与非公平锁
其实通俗意义来说就是“先来后到”。如果对一个锁来说,先对锁获取请求的线程一定会先被满足,后对锁获取请求的线程后被满足,那这个锁就是公平的。反之,那就是不公平的。
ReentrantLock支持非公平锁和公平锁两种。(可以根据构造方法的重载选择不同的锁)。
public class ThreadQuestion {
public static void main(String[] args) {
RockTest rockTest = new RockTest();
new Thread(rockTest, "线程1").start();
new Thread(rockTest, "线程2").start();
}
}
class RockTest implements Runnable {
private int i = 100;
//首先定义一个显示锁
private Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock(); // 获得锁
while (true) {
if (i <= 0) break;
i --;
if (i == 66) {
System.out.println(Thread.currentThread().getName() + ": i = " + i);
break;
}
System.out.println(Thread.currentThread().getName() + ": i = " + i);
}
lock.unlock(); // 释放锁
}
}
ReadWriteLock读写锁可以在同一时刻允许多个读线程访问,而读写锁可以在同一时刻允许多个读线程访问。Java提供了ReentrantReadWriteLock类作为读写锁的默认实现,内部维护了两个锁:一个读锁,一个写锁。通过分离读锁和写锁,使得在“读多写少”的环境下,大大地提高了性能。
5.1.3 volatile
volatile可以保证内存可见性且禁止重排序
可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取。
package com.zw.thread;
public class VolatileTest {
private volatile boolean flag = true;
public static void main(String[] args) {
VolatileTest test = new VolatileTest();
new Thread(() -> {
while(test.flag) {
System.out.println(Thread.currentThread().getName() + ": 正在执行" );
}
}, "线程1").start();
new Thread(() -> {
test.flag = false;
System.out.println(Thread.currentThread().getName() + ": 修改完了" );
}, "线程2").start();
}
}
线程1: 正在执行
线程2: 修改完了
而如果flag变量没有用volatile修饰,在main中修改的标志就不会更新到主内存。volatile保证了变量的可见性但是不能保证原子性。
5.2 死锁的问题
当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。
在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去,这种情况就是死锁形式
死锁的四个必要条件:
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 循环等待条件:第一个线程等待其他线程,后者又在等待第一个线程。
package com.zw.thread;
public class DeadLock {
public static void main(String[] args) {
final StringBuffer s1 = new StringBuffer();
final StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
// 先拿锁一,再拿锁二
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
// 上面线程1拿到了锁一让它睡眠使得线程2拿到锁二,这时候锁一在线程1,
// 线程2拿不到锁一,线程1拿不到锁二,互相等待造成死锁
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
破坏四个必要条件中的一个就可以阻止死锁问题。