内容
1.线程与进程
2.线程介绍与使用
关于线程内容也可参看我的另外几篇博客
https://www.jianshu.com/p/cb7e5dbe7968
https://www.jianshu.com/p/88d5e3bc726c
https://www.jianshu.com/p/2783773aa0d8
https://www.jianshu.com/p/cbc22e2f3be1
https://www.jianshu.com/p/84756fb84571
一.线程与进程
1.什么是进程
进程,就是正在运行/执行的一个程序。进程是用于管理其所有的资源的,不进行实际的任务。
2.什么是线程
刚才说了,进程不完成具体的任务,那么具体任务由谁来完成呢?没错,就是线程!
线程就是完成具体任务的。一个进程可以有多个线程。
3.实例说明
比如打开QQ,就是打开一个进程。但是我可以同时进行聊天、视频通话和刷QQ空间这三个任务,每一个任务都由一个特定的线程进行
在Java中,我们写的程序运行起来就是一个进程。
二.线程介绍与使用
1.主线程
主线程:就类似主路。main方法里面的代码是在主线程里面执行的。
还有手机什么都不动的时候,会展现出一个主界面,这个展现主界面的就是一个主线程。也就是说
①在Java里面:main方法里面的代码,就在主线程中跑
②Android/iOS是 :启动程序看到的UI界面就是UI主线程
2.子线程
子线程:除了主线程之外的都是子线程
3.多线程的作用
在主线程里面,任务的执行顺序是从上至下的。如果其中一个任务需要花费大量时间(比如下载一个很大的数据),那么这个任务后面的任务就会被阻塞(就类似堵车了),必须等这个任务结束才能被执行。用户的体验效果不好。这个时候就需要将耗时的任务放在另外一个不在主线程里面执行的路径(类似走小路,这就是子线程)
4.注意点
①主程序就是用来分配/调配的。
②不管是主线程还是子线程,它都有自己独立的内存空间,执行路径,生命周期
③Thread是管理线程的一个类,它继承Runnable接口
Thread.currentThread()可以获取当前线程的信息,这个方法也是很常用的
5.如何开启一个线程
1.写一个类继承Thread
①创建类继承Thread
重写父类的run方法,把具体执行的任务放在run方法里面
(其实run方法也可以通过对象.run()调用,但是如果这样调用的话,程序还是在主线程里面执行。这样就没啥意义了。所以不要这样做。start调用的话,系统会自动将这个任务放在队列中,等待调度,也就是等待操作系统调用。所以还是要用start方法去使用线程)
②创建类的对象
③调用start方法开始执行
2.写一个类实现Runnable接口
①创建一个类实现Runnable接口(这个类只是一个任务,并不能直接开启线程)
②创建任务类(也就是实现Runnable接口的那个类)的对象
③创建Thread类的对象(Thread类可以创建线程,我们只需要将自己的任务和Thread对象关联起来)
④调用start启动线程
6.继承Thread来使用线程
注意使用如何使用线程的构造方法以及getName方法的使用
import java.io.*;
import java.util.*;
public class 测试程序{
public static void main(String[] args){
System.out.println("main方法"+Thread.currentThread());
//2.创建具体的对象
TestThread testThread = new TestThread("子线程");
//3.启动线程
testThread.start();
}
}
//1.创建类继承Thread
class TestThread extends Thread{
public TestThread(String s){
super(s);
}
//执行的任务在run方法里
public void run() {
//下面是线程需要执行的任务
System.out.println("TestThread"+getName());
for(int i = 0;i < 100;i++) {
System.out.println(i+1);
}
}
}
输出
main方法Thread[main,5,main]
TestThread子线程
1
2
(后面的省略)
4.注意点(续)
线程是通过抢占时间片来得到运行机会的,谁抢到了时间片,谁就可以运行。这个时间片是由操作系统来分配的,所以每一次执行结果可能都是不一致的
如下面这段程序
import java.io.*;
import java.util.*;
public class 测试程序{
public static void main(String[] args){
System.out.println("main方法"+Thread.currentThread());
//2.创建具体的对象
TestThread testThread = new TestThread("子线程1");
TestThread testThread2 = new TestThread("子线程2");
//3.启动线程
testThread.start();
testThread2.start();
}
}
//1.创建类继承Thread
class TestThread extends Thread{
public TestThread(String s){
super(s);
}
//执行的任务在run方法里
public void run() {
//下面是线程需要执行的任务
for(int i = 0;i < 10;i++) {
System.out.println(getName()+":"+(i+1));
}
}
}
的结果是
main方法Thread[main,5,main]
子线程1:1
子线程2:1
子线程2:2
子线程1:2
子线程1:3
子线程1:4
子线程1:5
子线程1:6
子线程2:3
子线程2:4
子线程2:5
子线程2:6
子线程2:7
子线程2:8
子线程2:9
子线程1:7
子线程2:10
子线程1:8
子线程1:9
子线程1:10
但是下一次的结果就不一定了
也就是说,当调用start方法时,这个线程会自动扔到操作系统的任务队列中(线程池),至于这个任务什么时候被执行,我们无法确定,这一点由操作系统来决定。
7.实现Runnable接口来使用线程
注意如何将任务与Thread联系起来
import java.io.*;
import java.util.*;
public class 测试程序{
public static void main(String[] args){
System.out.println("main方法"+Thread.currentThread());
//2.创建具体对象
//这个其实是具体执行的任务,这个类不能直接开启线程,必须依赖于Thread类
TestRunnable testRunnable = new TestRunnable();
//3.创建一个Thread对象(因为只是实现Runnable,没有start方法)
//让这个线程去执行testRunnable的任务,也就是把它与testRunnable关联起来。这个线程名字为:子线程1
Thread thread = new Thread(testRunnable,"子线程1");
Thread thread2 = new Thread(testRunnable,"子线程2");
//不同的线程执行相同的任务,其实可以理解成不同的人开同样的车
//4.启动线程
thread.start();
thread2.start();
}
}
//1.创建一个类实现Runnable接口
class TestRunnable implements Runnable{
public void run() {
//这个线程需要执行的任务
for(int i = 0;i < 10;i++) {
System.out.println(Thread.currentThread().getName()+":"+(i+1));
}
}
}
输出结果
main方法Thread[main,5,main]
子线程1:1
子线程1:2
子线程1:3
子线程1:4
子线程1:5
子线程1:6
子线程1:7
子线程1:8
子线程1:9
子线程1:10
8.两种方式比较
Java不能多继承,但是可以实现多个接口
所以用实现Runnable接口的那个方式更灵活一点,更容易扩展。但是写起来稍微麻烦一点(其实也不是很麻烦,就比第一个方法多了一步):也就是说使用实现Runnable接口的方式使用线程更好。
即面向接口编程,能够松耦合。而第一种方式就不够灵活。
9.线程的生命周期
①new一个线程,就可以使线程处于创建状态
②使用start,线程就属于就绪状态。当抢占到时间片的时候,线程就属于运行状态,当失去时间片的时候,就再处于就绪状态。以此往返。
从就绪状态到运行状态是由操作系统来实现的,外部无法干预。
③当run方法结束的时候,线程就处于死亡状态了,这是正常结束。另外,手动让线程暂停,比如调用该线程的stop方法(不建议使用stop,因为该方法容易导致死锁),再或者线程抛出一个未捕获的Exception或Error的时候,线程也会处于死亡状态。
④当线程处于运行状态时,也有可能处于阻塞状态
(1)同步阻塞:使用synchronized就可以使线程处于同步阻塞状态,当锁解开的时候,就可以再返回就绪状态
(2)等待阻塞:使用wait()就可以使线程处于等待状态,等调用notify时,就可再返回就绪状态
(3)其他阻塞:使用sleep(),join()也可以使线程处于阻塞状态,等sleep休眠时间到,或者join()线程执行完毕,或者io流阻塞结束。就再返回就绪状态
源码关于线程状态的描述
这里我借用翻译工具翻译如下
10.线程常用方法
(1)如何让一个线程结束
①不要直接调用stop方法来结束一个线程
②好的方法应该是:自己写一个变量/标识符,用来标识线程结束的临界点
比如
class TestThread extends Thread{
private boolean shouldStop = true;
public TestThread(String s){
super(s);
}
//执行的任务在run方法里
public void run() {
//下面是线程需要执行的任务
while(shouldStop) {
System.out.println("子线程");
}
}
public void terminated() {
shouldStop = false;
}
}
(2)线程礼让和“插队“
①yield():线程礼让
礼让的线程会直接进入就绪状态,被礼让的线程并不一定会一直执行。如果礼让的线程再次获得时间片,则还会再次执行。所以,礼让是可能失败的。
使用示例
import java.io.*;
import java.util.*;
public class 测试程序{
public static void main(String[] args){
TestRunnable runnable = new TestRunnable();
Thread t1 = new Thread(runnable,"奔驰");
t1.start();
for(int i = 0;i < 200;i++) {
System.out.println("主线程"+(i+1));
if(i == 20) {
Thread.yield();//礼让
}
}
}
}
//1.创建一个类实现Runnable接口
class TestRunnable implements Runnable{
public void run() {
//这个线程需要执行的任务
for(int i = 0;i < 200;i++) {
System.out.println(Thread.currentThread().getName()+":"+(i+1));
}
}
}
②join():插队
可以使当前线程阻塞,插队的线程执行。这个基本是成功的。
使用示例
import java.io.*;
import java.util.*;
public class 测试程序{
public static void main(String[] args){
TestRunnable runnable = new TestRunnable();
Thread t1 = new Thread(runnable,"奔驰");
t1.start();
for(int i = 0;i < 200;i++) {
if(i == 20) {
try {
t1.join();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}//插队
}
System.out.println("主线程"+(i+1));
}
}
}
//1.创建一个类实现Runnable接口
class TestRunnable implements Runnable{
public void run() {
//这个线程需要执行的任务
for(int i = 0;i < 200;i++) {
System.out.println(Thread.currentThread().getName()+":"+(i+1));
}
}
}
11.多线程的优点和缺点
目前来说:
(1)优点:提高应用程序的使用率
(2)缺点:如果多个线程操作同一个资源,有可能出现不安全。所以需要解决这种问题。
12.保证线程安全的两种方式
①Lock 锁
必须使用的是同一个锁
class TestRunnable implements Runnable{
private static Lock lock = new ReentrantLock();
public void run() {
//这个线程需要执行的任务
for(int i = 0;i < 200;i++) {
lock.lock();//加锁
lock.unlock();//解锁
}
}
}
但是这种方式使用起来比较麻烦
②线程同步
必须保证锁的是同一个对象。 synchronized可以锁代码块和方法。
(1)第一种,随便弄个Object对象就可以实现同步
class TestRunnable implements Runnable{
private static Object obj = new Object();
public void run() {
//这个线程需要执行的任务
for(int i = 0;i < 200;i++) {
synchronized(obj) {
//这里面放被锁住的代码
}
}
}
}
(2)但是一般不这样搞。一般都是锁this
class TestRunnable implements Runnable{
private static Object obj = new Object();
public void run() {
//这个线程需要执行的任务
for(int i = 0;i < 200;i++) {
synchronized(this) {
}
}
}
}
(3)锁方法
class TestRunnable implements Runnable{
private static Object obj = new Object();
public void run() {
//这个线程需要执行的任务
for(int i = 0;i < 200;i++) {
test();
}
}
private synchronized void test() {
}
}
(4)注意点:
不管是锁代码块还是方法,都应尽量让锁的范围变小。
13.线程间通信
(1)线程与线程之间有时是需要通信、交互的。一般用到以下几个方法
①wait()让线程等待
②notify()唤醒某个线程
③notifyAll()唤醒多个线程
(2)注意:
①这三个方法不在Thread里面,而是在Object里面
②这三个方法必须由同步监视器来调用。(总之就是大家都在抢同一个资源)。
(3)使用示例
①要求:使用线程间通信,完成输出1 a 2 b。。。。。26 z的操作
②掌握使用匿名内部类创建线程的方法
③当锁代码块的时候,一般锁住Object对象,从而进行线程间通信。
④当锁方法的时候,是哪个对象调用这个方法就锁哪个对象。
⑤留意:Character.toChars()这个方法
这个方法在JDK帮助文档里是这样说的。
我去查了一下UTF-16,看不懂,但是我使用起来
这三个输出结果都是 a。所以目前来说,先不必计较。或者死记住:输出字符的时候,使用这个方法。(虽然我现在还没有查出,测试出不使用这个方法有什么危害)
import java.io.*;
import java.util.*;
public class 测试程序{
public static int state = 1;//1表示输出数字,2表示输出字母
public static void main(String[] args){
/* TestRunnable t = new TestRunnable();
Thread task = new Thread(t);
task.start();*/
//可以使用匿名内部类
/* Thread task = new Thread(new Runnable() {
public void run() {
for(int i = 0;i < 20;i++) {
System.out.println(Thread.currentThread().getName()+":"+(i+1));
}
}
});*/
Object obj = new Object();
new Thread(new Runnable() {
int num = 1;
public void run(){
//输出数字
while(true) {
synchronized(obj) {//争夺obj资源
//判断当前是不是在输出字母
if(state != 1) {
//当前线程需要等待一下
try {
obj.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//输出数字
System.out.println(num);
num++;
if(num > 26) {
break;
}
//唤醒当前obj锁上的其他等待的线程
state = 2;
obj.notify();
}
}
}
}).start();
new Thread(new Runnable() {
char alpha = 'a';
public void run() {
//输出字母
while(true) {
synchronized(obj) {//争夺obj资源
//判断当前是不是在输出数字
if(state != 2) {
//当前线程需要等待一下
try {
obj.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//System.out.println("程序执行到这里了");
//输出字母
System.out.println(Character.toChars(alpha));//Character.toChars(alpha)
alpha++;
if(alpha > 'z') {
break;
}
state = 1;
obj.notify();
}
}
}
}).start();
}
}
上面是锁的代码块,可以看出上面的代码不太简洁。下面使用锁方法的方式来完成同样的功能。
要记住:使用同一对象来调用方法。
import java.io.*;
import java.util.*;
public class 测试程序{
static Data d = new Data();
public static void main(String[] args){
new Thread(new Runnable() {
public void run() {
d.printNum();
}
}).start();
new Thread(new Runnable() {
public void run() {
d.printAlpha();
}
}).start();
}
}
class Data{
int num = 1;
int alpha = 'a';
int state = 1;
//此时锁的是当前类的对象,哪个对象调用这个方法就锁哪个对象
public synchronized void printNum() {
while(true) {
if(state != 1) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(num);
num++;
if(num > 26) {
break;
}
state = 2;
this.notify();
}
}
public synchronized void printAlpha(){
while(true) {
if(state != 2) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Character.toChars(alpha));
alpha++;//加的是ASCⅡ码
if(alpha > 'z') {//注意,这里是'z',而不是26
break;
}
state = 1;
this.notify();
}
}
}
总结
还是同样的感受:相比第一次学,这一次学的更多,更透彻了。也解决了很多问题。下一步就是多写代码,不断进行巩固!加油!!要学网络编程了!!激动!!