创建线程的五种方式
继承Thread
1.继承Thread并实现它的方法run(),方法里面是我们给线程的任务
static class Dee implements Thread {
@Override
public void run() {System.out.println("as");}
}
public static void main(String[] args) {
Dee dee = new Dee();
//线程启动
dee.start();
}
实现Runnable接口
2.Runnable接口,相当于创建一个任务,我们要执行这个任务的时候,就创建一个线程,然后传入这个任务—给线程一个任务
static class Dee implements Runnable {
@Override
public void run() {System.out.println("as");}
}
public static void main(String[] args) {
Dee dee = new Dee();
//通过构造方法将任务对象dee传入线程
Thread t1 = new Thread(dee);
t1.start();
}
匿名类实现
3.匿名类实现,只能创建一个执行指定任务的线程,若要创建多个重复任务的线程,则不该采取这种方式创建线程
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("as");
}
};
thread.start();
/************************************************/
//极端情况
new Thread(){
@Override
public void run() {
System.out.println("as");
}
}d.start();
}
实现Callable接口
Callable带返回值的线程
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Boolean> dee = new Dee();
FutureTask<Boolean> futureTask = new FutureTask<>(dee);
Thread thread = new Thread(futureTask);
//判断任务是否执行完毕
futureTask.isDone();
//可以主动取消任务,参数为true表示取消,返回值是boolean,返回true表示取消成功
futureTask.cancel(true);
thread.start();
//要先将线程启动,才能调用get方法,否则会阻塞在那里
boolean b = futureTask.get();
System.out.println(b);
}
static class Dee implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(i);
}
return true;
}
}
线程池
线程池–可以使线程重复使用,它也是一个线程,当任务执行完成之后,程序不会马上关闭,就是因为线程池还在等待接收,但若没有任务,一段时间之后会自动关闭。
缓存线程池,若线程池容量不足,扩容
ExecutorService service = Executors.newCachedThreadPool();
service.execute(任务对象(实现接口Runnable,重写run方法));
定长线程池,任务排队执行
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(任务对象(实现接口Runnable,重写run方法));
单线程线程池
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(任务对象(实现接口Runnable,重写run方法));
周期定长线程池
ScheduleExecutorService service = Executors.newScheduledThreadPool();
service.schedule(三个参数【自己看一下说明】)
service.scheduleAtFixedRate(四个参数)
线程的常见方法
设置和获取线程名称
1.设置和获取线程名称的两个方法——setName、getName
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
//得到当前线程的名称
System.out.println(Thread.currentThread().getName());
}
};
//设置线程的名称
thread.setName("110");
thread.start();
}
结果:
110
线程休眠
2.线程休眠方法——sleep
Thread thread = new Thread('传入某个任务');
thread.sleep(1000);//休眠1000毫秒即1秒
线程的中断
3.线程的中断:由线程自己决定是否中断。
因为若在外部直接掐死该线程,那么该线程就来不及释放某些资源,这将会导致内存垃圾,且垃圾可能无法被回收。
我们可以给线程(不是给任务对象)—打上中断标记。
然后当run方法里当调用sleep或wait方法时,线程会查看一下中断标记,
若标记为true,我们可以直接reuturn就结束线程,当然我们也可以不理会这个标记。
(注意当线程接收到标记为true,除了我们可以做些处理之外,该标记会自动改为false)
public static void main(String[] args) throws InterruptedException {
Dee dee = new Dee();
Thread thread1= new Thread(dee);
thread1.start();
for (int i=0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//给线程打上中断标记
thread1.interrupt();
}
//我们采用上面所说的创建线程的第二种方式,所以创建一个类实现Runnable接口
static class Dee implements Runnable {
@Override
public void run() {
for (int i=0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//当我们被打上标记,就会运行这里的代码块
System.out.println("我们被打上中断标记了,不管,我们改回来!");
//我们其实什么都没做,它自己就会改回来了
System.out.println("现在标记为:" + Thread.currentThread().isInterrupted());
}
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
结果:
Thread-0---0
main:0
Thread-0---1
main:1
Thread-0---2
main:2
Thread-0---3
main:3
main:4
Thread-0---4
我们被打上中断标记了,不管,我们改回来!
现在标记为:false
Thread-0---5
Thread-0---6
Thread-0---7
Thread-0---8
Thread-0---9
守护线程
4.守护线程:调用setDaemon(true);方法即可将线程设为守护线程,(注意参数为true时,才为守护线程)
用户线程:当进程里的所有用户线程结束,进程结束
守护线程:当所有用户线程结束,它就自动死亡
public static void main(String[] args) throws InterruptedException {
//通过匿名类实现来创建用户线程
new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}.start();
//创建守护线程
Thread daemonThread = new Thread() {
@Override
public void run() {
//设为无限循环,来试试守护线程是否会因为用户线程的结束而自动结束
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我还活着。。。haha");
}
}
};
//设置该线程为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
}
结果:
我还活着。。。haha
Thread-0: 0
我还活着。。。haha
Thread-0: 1
我还活着。。。haha
Thread-0: 2
我还活着。。。haha
Thread-0: 3
我还活着。。。haha
Thread-0: 4
我们可以注意到,当用户线程结束后,守护线程也自动结束了,尽管它的run()方法是while(true)来进行无限循环,但可惜,一旦当所有的用户线程结束后,它也会自动结束。
同步代码块和同步方法
5.同步代码块和同步方法:要有锁对象的概念
当线程想要执行被锁对象锁住的代码块,就必须得到锁对象。若锁对象被其他线程抢到,则该线程等待,直到下次抢夺开始。抢到了就执行代码块,没抢到就继续等待,直到抢到为止。
下面就是由两个线程来输出0到4的数字,规则:必须有序且不重复。
public static void main(String[] args) throws InterruptedException {
Dee dee = new Dee();
Thread t1 = new Thread(dee);
Thread t2 = new Thread(dee);
t1.start();
t2.start();
}
static class Dee implements Runnable {
private Object object = new Object();
private int i;
@Override
public void run() {
for (i = 0; i < 5; i++) {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果:
Thread-0: 0
Thread-1: 0
Thread-1: 2
Thread-0: 2
Thread-1: 4
Thread-0: 4
以上的案例出现错乱,是因为for循环时,两个判断的时机不对。
案例分析:
当线程A判断循环,此时i=0,i<5,判断可以进,线程A就执行循环体里的代码。
但几乎时同一时刻,线程B也在判断,而线程A并没有走完循环,好让i=1。所以此时线程B判断的也是i=0,i<5,可以进。
此时线程A可能刚println打印完Thread-0: 0,正在休眠中。紧接着线程B也开始打印,此时线程A还没有第二次循环,说明i还是等于0,所以线程B也是输出Thread-1: 0。
以下是正确的案例:
static class Dee implements Runnable {
private Object object = new Object();
//若是不赋值,默认为0
private int i;
@Override
public void run() {
while (true) {
//我们用object这个对象来做锁对象
synchronized (object) {
if (i >= 5) break;
System.out.println(Thread.currentThread().getName() + ": " + i);
i++;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果:
Thread-0: 0
Thread-1: 1
Thread-1: 2
Thread-0: 3
Thread-1: 4
由以上的两个案例,可以提示到我们,当我们使用同步方法或代码块的时候就要慎重考虑采用for,还有仔细思考不同线程执行的时机。
同步方法的案例:
static class Dee implements Runnable {
//若是不赋值,默认为0
private int i;
@Override
public void run() {
while (true) {
if (print()) break;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized boolean print() {
if (i >= 5) return true;
System.out.println(Thread.currentThread().getName() + ": " + i);
i++;
return false;
}
}
注意:同步方法锁的是this,即该任务对象。
显示锁
Lock接口可以new一个它的子类ReentrantLock得到显示锁。
public static void main(String[] args) throws InterruptedException {
Dee dee = new Dee();
Thread t1 = new Thread(dee);
Thread t2 = new Thread(dee);
t1.start();
t2.start();
}
static class Dee implements Runnable {
//若是不赋值,默认为0
private int i;
//声明一个显示锁
Lock l = new ReentrantLock();
@Override
public void run() {
while (true) {
l.lock();
if (i >= 5) return;
System.out.println(Thread.currentThread().getName() + ": " + i);
i++;
l.unlock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果:
原因是当我们的 if (i >= 5)为真时,return。此时虽然该线程结束了,但是他并没有释放锁对象,导致另一个线程一直在阻塞。
正确的案例:
static class Dee implements Runnable {
//若是不赋值,默认为0
private int i;
//声明一个显示锁
Lock l = new ReentrantLock();
@Override
public void run() {
while (true) {
l.lock();
if (i >= 5){
l.unlock();
return;
}
System.out.println(Thread.currentThread().getName() + ": " + i);
i++;
l.unlock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果:
Thread-1: 0
Thread-0: 1
Thread-1: 2
Thread-0: 3
Thread-1: 4
公平锁:线程排队,不会出现一个线程连续抢到cpu时间片
static class Dee implements Runnable {
//若是不赋值,默认为0
private int i;
//声明一个显示锁
Lock l = new ReentrantLock(true);
@Override
public void run() {
while (true) {
l.lock();
if (i >= 5){
l.unlock();
return;
}
System.out.println(Thread.currentThread().getName() + ": " + i);
i++;
l.unlock();
}
}
}
结果:
Thread-1: 0
Thread-0: 1
Thread-1: 2
Thread-0: 3
Thread-1: 4
注意:以上程序的sleep语句,除了中断标记的方法要用到,其他的都是为了要差距明显,体现线程抢占式的cpu时间分配。
补充:线程的阻塞,当因某件事情比较消耗时间就可称之为阻塞:比如读取文件,或等待用户输入等。