创建和运行线程
方法一:直接使用Thread
// 构造方法的参数是给线程指定名字,,推荐给线程起个名字
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
方法二:使用Runnable配合Thread
把【线程】和【任务】(要执行的代码)分开,Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐给线程起个名字
Thread t2 = new Thread(task2, "t2");
t2.start();
方法三:FutureTask配合Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 实现多线程的第三种方法可以返回数据
FutureTask futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("多线程任务");
Thread.sleep(100);
return 100;
}
});
// 主线程阻塞,同步等待 task 执行完毕的结果
new Thread(futureTask,"我的名字").start();
log.debug("主线程");
log.debug("{}",futureTask.get());
}
注意:get()方法会阻塞线程,等待任务返回结果
常用方法
sleep与yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
- 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
- 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)
interrupt方法
- 打断sleep的线程,会清空打断状态
- 打断正常运行的线程,不会清空打断状态,正常运行被打断后,可由线程自己通过打断状态判断后续操作
- 打断park进程,不会清空打断状态
守护线程
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true); 方法变成守护线程
垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
synchronized
临界区
- 一个程序运行多线程本身是没有问题的
- 问题出现在多个线程共享资源的时候(读写)
- 先定义一个叫做临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区
竞态条件
多个线程在临界区执行,由于代码指令的执行不确定而导致的结果问题,称为竞态条件
synchronized实现阻塞式解决
synchronized(对象) // 线程1获得锁, 那么线程2的状态是(blocked)
{
临界区
}
保证了临界区代码的原子性
synchronized加在方法上
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
//------------------------------------------------------------------------------------------------
class Test{
public synchronized static void test() {
}
}
// 等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
- 分为类锁和对象锁
- 使用对象锁的情况,只有使用同一实例的线程才会受锁的影响
- 类锁是所有线程共享的锁,所以同一时刻,只能有一个线程使用加了锁的方法或方法体,不管是不是同一个实例。
- 静态变量和类信息一样也是存在方法区的并且整个 JVM 只有一份,所以加在静态变量上可以达到类锁的目的
- 类锁和对象锁不互相影响
park&unpark
与Object的wait&notify相比:
- wait,notify必须配合Object Monitor一起使用,而park、unpark不必
- park&unpark是以线程为单位来阻塞和唤醒线程,比notify的随机唤醒精确
- park&unpark可以先unpark
变量的线程安全
成员变量和静态变量的线程安全
- 如果没有变量没有在线程间共享,那么变量是安全的
- 如果变量在线程间共享
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量的线程安全
线程安全的情况
局部变量被初始化为基本数据类型,此时是安全的
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
线程不安全的情况
如果局部变量引用的对象逃离方法的范围,那么要考虑线程安全
public class Test15 {
public static void main(String[] args) {
UnsafeTest unsafeTest = new UnsafeTest();
for (int i =0;i<100;i++){
new Thread(()->{
unsafeTest.method1();
},"线程"+i).start();
}
}
}
class UnsafeTest{
ArrayList<String> arrayList = new ArrayList<>();
public void method1(){
for (int i = 0; i < 100; i++) {
method2();
method3();
}
}
private void method2() {
arrayList.add("1");
}
private void method3() {
arrayList.remove(0);
}
}
method2和method3中的arryList前都省略了th