线程的基本使用
1.继承Thread
public class ThreadTest2 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"=>实现run方法");
}
public static void main(String[] args) {
ThreadTest2 myThread=new ThreadTest2();
myThread.start();
}
}
2.实现runable接口
public class RunableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "=>实现run方法");
}
public static void main(String[] args) {
RunableImpl runable = new RunableImpl();
Thread t1 = new Thread(runable);
Thread t2 = new Thread(runable);
t1.start();
t2.start();
}
}
3.使用线程池
import java.util.concurrent.*;
public class CallableTest implements Callable<String>{
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个有10个线程的线程调度服务
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
for (int i = 0; i < 100; i++) {
//提交100个任务,让调度服务执行
scheduledExecutorService.submit(new CallableTest());
}
}
@Override
public String call(){
System.out.println(Thread.currentThread().getName()+"执行call()");
//call方法可以有返回值,这个在线程池中再详细说
return "";
}
}
线程的生命周期
线程常用方法
方法签名 | 说明 | 备注 |
synchronized void start() | 启动线程 | start方法只能被调用一次,也就是说一个线程只能被启动一次。如果start被调用1次以上则会抛出异常IllegalThreadStateException |
void run() | 线程启动成功后会自动调用run方法 | Thread类默认实现了Runable接口,如果创建线程时没有指定Runable对象或者Thread子类没有重写run方法,则会执行Thread类实现的run方法,该方法将不执行任何动作 |
void sleep(long millis)
void sleep(long millis, int nanos) | 线程休眠指定毫秒 线程休眠毫秒+纳秒 | 线程休眠后进入阻塞状态,休眠不会释放锁 |
暂停这个线程 | suspend方法已经不推荐使用,因为suspend容易导致线程死锁。 简单分析一下 suspend和sleep的区别 虽然suspend和sleep都可以导致线程等待,而且两个方法都不会释放已经持有的监视器(锁),但是为什么suspend更容易导致死锁的发生呢? suspend方法可以在线程外部调用,无论当前线程执行到什么代码都会被叫停,这就引发了区别,因为sleep只能暂停当前线程,也就是说只能在线程内部显示的暂停,这个时候线程持有哪些对象锁是明确知道的。 但是如果调用suspend那么线程对于我们来说就是一个黑匣子,它运行到哪里,当前持有什么对象锁,这些锁在后面的代码中是否需要都不知道,这就很可能在一些关键资源上被一直持有不能释放从而导致死锁。 如最常见的 System.out.println()方法 这个方法就是被synchronize修饰的,如果线程在执行 System.out.println()时被调用suspend暂停,后续的代码中如果在调用 System.out.println()就会进入死锁。 | |
恢复挂起的线程 | 由于suspend方法不安全所以对应的resume方法也不在推荐使用 | |
void wait() | 暂停线程,释放当前线程持有的锁 | wait/notify 这对方法总是同步出现用来实现线程之间的协作与通信。使用注意事项 1、这两个方法只能在synchronized作用域内调用才有效,在没有获取到对象锁的地方调用会抛出异常IllegalMonitorStateException 2、一个监视器(锁)只能唤醒它这个监视器本身挂起的线程,不能唤醒其他监视器挂起的线程 3、为什么要设计成先获取锁才能去调用wait与notify方法 个人理解: 1、方便通过相同的监视器协同相关的线程进行工作,比如notifyAll方法不会唤醒所有等待的线程,而是唤醒被同监视器挂起的线程 2、因为wati与notify在同步代码块中从而避免了同时接收到指令而引起的并发问题,如果没有这个限制一个线程被另外两个线程分别调用wait和notify方法就会引起并发问题 |
native void notify() | 唤醒一个被wait方法等待的线程,当前线程不会立刻释放对象锁 | |
native void yield() | 放弃当前cpu资源 | 线程放弃cpu资源后,可能马上又获得cpu资源 |
void join() | 等待线程对象销毁 | join方法中有一个while循环,改循环只有在线程死亡后(died)才会结束,也是因为这一点join方法的调用会导致调用者进入等待状态,其实就是join方法一直没有执行完。利用这个特点可以在一些场景中等待线程执行完成,在执行其他操作。但是通过对源码的分析可以看到,join方法会一直获取锁释放锁,这样的方法是比较耗性能的。 |
void stop() | 强制线程停止执行。 | stop方法已经被标记为不推荐使用,引用API中的说明: 使用stop停止线程。会导致它解锁所有已锁定的监视器(这是未检查的ThreadDeath异常向上传播堆栈的自然结果)。如果以前受这些监视器保护的任何对象处于不一致的状态,则被损坏的对象对其他线程可见,可能导致任意行为。 |
wati 与notify使用方式
public class WaitNotifyTest extends Thread {
private Object lock = new Object();//定义对象锁
private volatile int threadSwitch = 1;//控制等待的开关
private String id;//线程id
public WaitNotifyTest(String id) { this.id = id; }
@Override
public void run() {
while (true) {
synchronized (lock) { //先获取锁在调用wait()
try {
Thread.sleep(100);
System.out.println("run id=" + id);
if (threadSwitch == 2) { lock.wait(); }
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
public void off() { this.threadSwitch = 2; }
public void on() {
synchronized (lock) { //先获取锁在调用notifyAll
this.threadSwitch = 1;
lock.notifyAll();
System.out.println("唤醒id=" + id);
}
}
public static void main(String[] args) throws InterruptedException {
WaitNotifyTest t1 = new WaitNotifyTest("1");
WaitNotifyTest t2 = new WaitNotifyTest("2");
//启动两个线程
t1.start();
t2.start();
Thread.sleep(1000);
//使线程进入等待状态
t1.off();
t2.off();
Thread.sleep(1000);
//唤醒线程1
t1.on();
}
}
输出结果:
run id=2
run id=1
run id=2
run id=1
run id=1
run id=2
run id=2
run id=1
run id=1
run id=2
run id=1
run id=2
run id=1
run id=2
run id=1
run id=2
run id=2
run id=1
run id=1
run id=2
唤醒id=1 // 从后面的输出结果可以看出,调用notifyAll()只唤醒了线程1,线程2继续处于等待状态
run id=1
run id=1
run id=1
run id=1
run id=1
run id=1
run id=1
run id=1
java线程与操作系统内核线程的关系
java中的线程是直接映射到操作系统中的线程的,我们来验证一下,我们在代码中创建1000个线程,然后看操作系统中的线程数量的变化
public class ThreadTest1 {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
Thread.sleep(1000*20);
}
}
在windows系统中运行之前线程数为3909
运行代码后
由此可以说明虚拟机是直接在内核中创建了线程,我们在linux系统中在测试一下
在linux系统 中运行
查看进程id
找到进程id为27418
在通过top命令查看这个进程的详细信息
同top命令可以看到27418进程中有1020个线程,说明我们创建的1000个线程也映射到了操作系统内核中。
通过查看Thread类的源码可以看到start方法中通过调用一个被 native 修饰的start0方法启动线程,这也说明了启动线程是会调用操作系统本地方法的。
总结:由于java对于线程的启动,销毁,需要调用系统内核资源,这个过程是需要耗费很多资源的,所以在程序中不能无限制的去创建线程,最好使用可控的线程池来完成任务的调度。