1. 线程与进程
- 进程是系统资源分配与调度的基本单位
- 线程是进程的一个执行路径,一个进程中至少有一个线程
- cpu资源特殊,它是分配到进程的,但是真正占用cpu资源的是线程,所以说线程是cpu分配的基本单位。
- 在java中,启动main函数就是启动了一个进程,main函数所在的线程就是进程中的一个线程,称为主线程。
- 堆和方法区被所有线程共享。堆中主要存放new操作创造的对象实例;方法区中存放jvm加载的类、常量以及静态变量等信息。
- 每个线程各自享有栈和程序计数器。栈用来存放局部变量;程序计数器用来存放下一条指令的地址。
2. 线程的创建和运行
java中有三种方式,分别为继承Thread类并重写run方法、实现Runnable接口中的run方法和使用FutureTask的方法。
- 继承Thread类并重写run方法
class MyTheard extends Thread{
@Override
public void run() {
System.out.println("child thread");
}
}
public class Main {
public static void main(String[] args) {
MyTheard myTheard = new MyTheard();
myTheard.start();
}
}
- 实现Runnable接口中的run方法
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("child thread");
}
}
public class Main {
public static void main(String[] args) {
Thread myThread2 = new Thread(new MyThread2());
myThread2.start();
}
}
- 使用FutureTask
class Mythread3 implements Callable{
@Override
public Object call() throws Exception {
return "child";
}
}
public class Main {
public static void main(String[] args) {
FutureTask<String> stringFutureTask = new FutureTask<>(new Mythread3());
new Thread(stringFutureTask).start();
try {
String result = stringFutureTask.get();
System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
既然有三种多线程的方法,那就会各有优缺点。 使用继承的好处是方便传参。但是由于java不支持多继承,如果继承了Thread则不能继承其他类。而实现Runnable接口中的run方法则没有这个限制。前两种方式都没有办法拿到任务的返回结果,Futuretask方式可以。
3. 线程等待与通知
- 某线程的wait()方法阻塞线程,调用某线程的notify()方法或者notyfiall()方法返回,或者其他线程调用该线程的interrupt()方法,该线程抛出InterruptedException异常。
- 使用wait()的时候需要事先获取到该对象的监视器锁。有如下两种方式。
- 执行synchronized同步代码块,使用改共享变量作为参数
synchronized(共享变量){
}
- 调用该共享变量的方法,方法使用synchronized修饰
synchronized void add(int a, int b){
}
- 虚假唤醒问题:即使没有进行通知、中断或者等待超时,线程自己从挂起状态被唤醒。解决方法是将wait操作放在一个while循环中。
synchronized(obj){
while(条件不满足){
obj.wait();
}
}
- 生产者消费者模型
public class GandC {
static final Queue queue = new LinkedList<>();
static final int MAX_SIZE = 10;
public static void main(String[] args) throws InterruptedException {
Thread producer = new Thread(() -> {
for (int i = 0; i < 100; i++) {
synchronized (queue) {
while (queue.size() == 10) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(1);
System.out.println("produce 1, exist " +queue.size());
queue.notifyAll();
}
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 100; i++) {
synchronized (queue) {
while (queue.size() == 0) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll();
System.out.println("consume 1, exist "+queue.size());
queue.notifyAll();
}
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
- wait(long timeout) 没有在timeout时间内被唤醒,会因为超时而返回。wait()函数内部调用wait(0)
- wait(long timeout int nanos) 内部调用wait(long timeout), 当nanos>0时,参数timeout递增一
- notify() 使用共享对象的notify方法之后,会随机唤醒一个调用该共享变量wait()方法的线程。获得监视器锁之后才可以调用。
- notifyAll() 唤醒所有调用该共享变量wait()方法的线程。
- join() 等待线程完成再继续往下执行
- sleep() 让出指定时间的执行权,在这期间不参与cpu的调度,但是持有锁不让出。
4. 让出cpu执行权的yield方法
一个线程中调用yield方法,就是请求出让自己的cpu使用,但是线程调度器可以无视这个暗示。
yield和sleep的区别: yield是出让自己的cpu使用权,但是没有被阻塞挂起,处于就绪状态 sleep则是被阻塞挂起
5. 线程中断
java中的线程终端是一种协作模式,设置线程的中断标致并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
- void interrupt()方法:中断线程。但仅仅是设置一个标识。若A线程调用wait、join、sleep函数被挂起,调用interrupt()函数则会抛出InterruptedException异常。
- boolean isInterrupted() 方法。检测当前线程是否被中断,不清除中断标识。
- boolean interrupted()方法。检测当前线程(与调用此函数的线程无关)是否被中断,清除中断标识。
一个根据中断标识判断线程是否终止的例子
public class interrupt {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("thread begin");
}
System.out.println("thread end");
}
});
thread.start();
Thread.sleep(1000);
System.out.println("interrupt");
thread.interrupt();
thread.join();
System.out.println("main thread end");
}
}
6. 线程死锁
线程死锁是指两个或两个以上的线程在争夺资源的过程中互相等待的现象。 产生线程死锁有四个条件
- 互斥条件。一个资源只能同时被一个线程所使用。
- 请求并持有条件。一个线程在持有一个资源后仍然请求资源。
- 不可剥夺条件。一个线程持有某个资源后不可被剥夺。只有在自己使用结束之后才由自己释放。
- 环路等待条件。发生死锁后,必然存在一个线程-资源的环形链。
由此引发一个问题:如何避免死锁? 想要避免死锁,破坏掉一个条件即可。目前只有请求并持有条件和环路等待条件是可以被破坏的。 资源的有序分配可以避免死锁。
7.守护线程
线程分为守护线程(daemon thread)以及用户线程(user thread) 守护线程伴随用户线程而生存。当用户线程结束后,jvm就会正常退出,而不管当前是否存在守护线程。如main函数所在的线程就是一个用户线程,而jvm内部还启动了好多守护线程,比如垃圾回收线程等。 创建守护线程的代码如下
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread begin");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread end");
}
});
thread.setDaemon(true);
thread.start();
Thread.sleep(100);
}
}
8. ThreadLocal
有没有一种方式是当创建一个变量之后,每个线程对其进行访问的时候访问的是自己的变量呢?使用ThreadLocal! 使用代码如下
public class ThreadLocalTest {
static ThreadLocal<String> localVariable = new ThreadLocal<>();
static void print(String str){
System.out.println(str + ":" + localVariable.get());
localVariable.remove();
}
public static void main(String[] args) {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("A variable");
print("A");
System.out.println("after remove:" + localVariable.get());
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("B variable");
print("B");
System.out.println("after remove:" + localVariable.get());
}
});
A.start();
B.start();
}
}
每个线程内部都有一个名字为threadLocals的成员变量,该变量的类型为HashMap。其key为我们定义的ThreadLocal变量的this引用,value是我们设置的值。若线程一直存在,这些变量也一直存在,所以可能会造成内存溢出。因此使用完需要及时的使用remove方法删除。
9.InheritableThreadLocal
使用ThreadLocal可以让每个线程拥有自己的副本。但是父线程无法访问子线程的值。这时,使用InheritableThreadLocal便可以实现这个功能。 其原理就是子线程在创建时会找到父线程的InheritableThreadLocal这个变量。若是不为null,说明存在,则复制一份给到子线程中。
|