文章讲述线程相关知识。包括进程,线程定义,线程的创建过程,线程常见方法,线程的生命周期,线程安全问题,线程间通信,生产者消费者问题,线程卖票程序
一、预备知识
程序:程序是指完成特定任务,用某种语言编写的一组指令集合。指一段静态的代码。
进程:正在运行的程序。是操作系统资源分配的最小单位。
线程:进程进一步细分为线程,是一个进程内部最小执行单元。是操作系统任务调度的最小单元。可以独立运行,不可脱离进程。
进程中可以包含多个线程,多个线程共享同一个进程的内存资源。进程必须包含一个线程(主线程),在主线程中可以创建并启动其它线程。线程不可以脱离进程运行。
二、线程的相关知识
1.继承Thread类
代码如下:
public class ThreadDemo extends Thread{
@Override
public void run() {
super.run();
//继承Thread类,重写润方法,在run方法里面添加任务代码
System.out.println(Thread.currentThread().getName()+"正在运行...");
}
}
2.实现Runnable接口
代码如下:
public class RunnableDemo implements Runnable{
@Override
public void run() {
//实现Runnable接口,重写run方法
System.out.println(Thread.currentThread().getName()+"正在运行...");
}
}
3.实现Callable接口
这种方式创建线程,线程执行完毕可以带返回值,并且支持泛型,自定义返回值类型。也支持异常,扩展性更强。
代码如下:
public class CallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
CallableThread callableThread = new CallableThread();
FutureTask<Integer> task = new FutureTask<>(callableThread);
Thread thread = new Thread(task);
thread.start();
try {
Integer sum = task.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
4.开启线程
代码如下:
public class Test {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
5.线程中的常用方法
代码如下:
//run()方法是执行线程任务
//start()方法是开启一个线程
//setName()给线程设置名字,也可以通过Thread类的构造器设置名字
//Thread.currentThread()获取当前正在运行的线程
//getName()获取线程的名字
//线程一共有10级,1-10,等级越高,优先权越大,越容易被CPU调度,线程默认的优先权是5
//setPriority()设置线程的优先权
//getPriority()获取线程的优先权
6.加入线程后的TCP聊天程序
代码如下:
public class SendThread extends Thread {
private Socket socket;
public SendThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行:");
try {
OutputStream out = socket.getOutputStream();
DataOutputStream dout = new DataOutputStream(out);
Scanner scanner = new Scanner(System.in);
while (true) {
String msg = scanner.next();
dout.writeUTF(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ReceiverThread extends Thread{
private Socket socket;
public ReceiverThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行:");
try {
InputStream in = socket.getInputStream();
DataInputStream din = new DataInputStream(in);
while(true){
String msg = din.readUTF();
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9966);
Socket socket = serverSocket.accept();
ReceiverThread receiverThread = new ReceiverThread(socket);
receiverThread.setName("接收端线程");
receiverThread.start();
SendThread sendThread = new SendThread(socket);
sendThread.setName("发送端线程");
sendThread.start();
}
}
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9966);
SendThread sendThread = new SendThread(socket);
sendThread.setName("发送端线程");
sendThread.start();
ReceiverThread receiverThread = new ReceiverThread(socket);
receiverThread.setName("接收端线程");
receiverThread.start();
}
}
7.线程的生命周期图
笔者使用自定义类继承Thread类,重写run()方法,创建自定义线程。
当新建一个自定义类对象的时候是线程的新建状态,表示已经有线程对象,但是这个线程无法获取CPU调度。
当调用start()方法后,线程进入就绪状态,表示这个线程有获取CPU调度的机会。
当线程获取到CPU调度,则线程处于运行状态,CPU执行时间片用完或者调用yield()方法,则线程再次进入就绪状态,等待CPU调度。
若线程在运行状态时程序调用sleep()方法,或wait()方法,或join()方法,或等待同步锁,则线程进入阻塞状态。
当sleep时间到,执行notify()/或notifyAll(),join结束,获取到同步锁,则线程进入就绪状态,等待CPU调度。
当线程将run()方法执行完,或调用stop()方法,或遇到ERROR/EXCEPTION且为处理,则线程消亡。
8.线程的分类
在Java里面将线程分为守护线程与用户线程。
守护线程:当程序中的所有用户线程都执行完毕,则守护线程也会结束。
代码如下:
ThreadDemo threadDemo = new ThreadDemo();
//将该线程设置为守护线程
threadDemo.setDaemon(true);
threadDemo.start();
三、多线程
程序需要同时执行多个任务。
程序需要实现一些需要等待的任务。
需要执行后台程序。
多线程提高程序响应。
提高CPU利用率。
改善程序结构。
多线程需要协调和管理,所以需要CPU时间跟踪线程。
多线程之间访问共享资源,会相互产生影响,必须解决竞用共享资源的问题。
并发:是指一个CPU在同一时间段内依次执行多个任务。
并行:是指多个CPU在同一时刻同时执行多个任务。
1.使用多线程模拟买票程序
我们模拟买票程序,以下程序是未加入同步机制的代码。
我们使用static变量num表示共享资源。num表示票的代号。
开启了两个线程对共享资源同时访问。
2.问题所在
此时票的数量是共享资源,所有的线程对象共同访问同一个票数量。但是我们CPU的执行权是由操作系统决定,我们无法控制CPU先来后到的执行线程。如:窗口1线程买到了票10,此时票的数量还未减1,变成9(下一次只可以卖出票9,不可以卖出票10,因为票10已经售出。),但是在这个时候窗口1线程CPU执行权被剥夺,窗口2获得CPU执行权。此时票10在下一次执行的时候,窗口2会以为票10存在,继续由窗口2线程买到票10。票10被重复卖出,出现问题。
使用Thread模拟代码如下:
public class ThreadDemo extends Thread {
static int num = 10;
@Override
public void run() {
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "买到了票" + num);
num--;
} else {
break;
}
}
}
}
public class Test {
public static void main(String[] args) {
ThreadDemo threadDemo1 = new ThreadDemo();
threadDemo1.setName("窗口1");
threadDemo1.start();
ThreadDemo threadDemo2 = new ThreadDemo();
threadDemo2.setName("窗口2");
threadDemo2.start();
}
}
使用Runnable模拟代码如下:
public class RunnableDemo implements Runnable{
int num = 10;
@Override
public void run() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true){
if (num> 0 ){
System.out.println(Thread.currentThread().getName()+"买到了票"+num--);
}else {
break;
}
}
}
}
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread1 = new Thread(runnableDemo, "窗口1");
Thread thread2 = new Thread(runnableDemo, "窗口2");
thread1.start();
thread2.start();
}
}
代码执行结果如下:
3.解决重复卖出同一个票问题
我们的解决办法是:加入同步机制。
当多个线程对同一资源(共享资源)进行读写的时候,会引起错误,我们必须使用同步机制,使多个线程先来后到依次访问共享资源。同步就是加锁和排队。 使用锁的机制,我们给共享资源加入锁,当存在正在访问共享资源线程时,我们这个线程处于加锁状态,此时当其他线程访问共享资源的时候,会等待同步锁结束才可以访问同步资源。实现对共享资源的依次访问,同一时刻只会有一个线程读写共享资源。
使用Thread解决代码如下:
public class ThreadDemo extends Thread {
static int num = 10;
static Object obj = new Object();
@Override
public void run() {
while (true) {
try {
//这个地方加入sleep函数让我们的线程休眠1秒,使其他线程有机会获得CPU执行权,让我们的程序更直观的
//表现出使用同步机制之后的效果
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步代码块如下,obj是一个同步对象(线程中只存在一份的对象),多个线程访问的是同一个对象
//这个同步对象不可以是this,因为this代表的是当前对象,每个线程访问的时候,都是一个新的线程对象。
//与同步对象的定义相悖。
synchronized (obj){
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "买到了票" + num);
num--;
} else {
break;
}
}
}
}
}
public class Test {
public static void main(String[] args) {
ThreadDemo threadDemo1 = new ThreadDemo();
threadDemo1.setName("窗口1");
threadDemo1.start();
ThreadDemo threadDemo2 = new ThreadDemo();
threadDemo2.setName("窗口2");
threadDemo2.start();
}
}
使用Runnable解决代码如下:
public class RunnableDemo implements Runnable {
int num = 10;
@Override
public void run() {
while (true) {
synchronized (this) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "买到了票" + num--);
} else {
break;
}
}
}
}
}
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread1 = new Thread(runnableDemo, "窗口1");
Thread thread2 = new Thread(runnableDemo, "窗口2");
thread1.start();
thread2.start();
}
}
代码执行结果如下:
4.同步机制的影响
一个线程持有锁会使其他需要此锁的线程挂起;在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延迟,引起性能问题。
5.Lock(锁)
Lock是一个接口,我们使用ReentrantLock类进行实现。
我们可以使用ReentrantLock的实例进行显示的加锁和释放锁。
使用ReentrantLock给程序加锁,代码如下:
public class ThreadDemo extends Thread {
static int num = 10;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
Thread.sleep(1000);
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "买到了票" + num);
num--;
} else {
break;
}
} catch (InterruptedException e) {
lock.unlock();
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public class Test {
public static void main(String[] args) {
ThreadDemo threadDemo1 = new ThreadDemo();
threadDemo1.setName("窗口1");
threadDemo1.start();
ThreadDemo threadDemo2 = new ThreadDemo();
threadDemo2.setName("窗口2");
threadDemo2.start();
}
}
6.死锁
两个线程彼此等待对方的锁,造成程序无法向前执行的现象。
代码如下:
public class DieThread extends Thread{
static Object objA = new Object();
static Object objB = new Object();
boolean flag;
public DieThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (objA){
System.out.println("if objA");
synchronized (objB){
System.out.println("if objB");
}
}
} else {
synchronized (objB){
System.out.println("else objB");
synchronized (objA){
System.out.println("else objA");
}
}
}
}
}
public class DieThreadTest {
public static void main(String[] args) {
DieThread thread1 = new DieThread(true);
thread1.start();
DieThread thread2 = new DieThread(false);
thread2.start();
}
}
7.线程间通信
线程通信指的是多个线程相互牵制,相互调度,即线程间的相互作用。
涉及三个方法:
wait():让当前线程进入阻塞状态,并释放同步监视器。
notify():唤醒被wait的线程,如果有多个线程被wait,则唤醒优先级最高的线程。
notifyAll():唤醒所有被执行wait的线程。
这三个方法必须在同步代码块或者同步方法中使用。
代码如下:
public class CommunicationThread extends Thread {
static int num = 1;
static Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) {
object.notifyAll();
if (num==101){
break;
}
System.out.println(Thread.currentThread().getName() + ":" + num++);
try {
Thread.sleep(100);
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Test {
public static void main(String[] args) {
CommunicationThread thread1 = new CommunicationThread();
thread1.start();
CommunicationThread thread2 = new CommunicationThread();
thread2.start();
}
}
运行结果如下:
该程序会使用两个线程交替打印1~100的数字。
8.生产者与消费者问题
现在有一个柜台,一个消费者,一个生产者,当柜台没有商品的时候我们需要让生产者生产商品,当存在商品时我们需要消费者消费商品。
代码如下:
public class Counter {
int num = 1;
public synchronized void custom() {
if (num > 0) {
num--;
System.out.println("消费者消费商品:" + num);
this.notify();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void produce() {
if (num == 0) {
num++;
System.out.println("生产者生产商品:" + num);
this.notify();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Custom extends Thread {
Counter counter;
public Custom(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
counter.custom();
}
}
}
public class Produce extends Thread {
Counter counter;
public Produce(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
counter.produce();
}
}
}
public class Test {
public static void main(String[] args) {
Counter counter = new Counter();
Custom custom = new Custom(counter);
custom.start();
Produce produce = new Produce(counter);
produce.start();
}
}
总结
为完成指定任务,使用特定语言编写的静态代码称之为程序。正在运行的程序称之为进程。CPU将资源分配给进程,进程之下是线程,所有线程共享进程的的CPU资源,线程是依赖于进程,是CPU调度的最小单元,线程可以独立运行。
线程的生命周期包括:创建,就绪,运行,阻塞,死亡。
引用多线程机制是为了提高CPU利用率更加快捷的执行程序,完成任务。
但是多线程并行访问共享资源的时候会存在冲突问题,可以使用同步机制解决。
同步机制就是排队和加锁,让线程依次执行,同一时刻只存在一个线程访问共享资源。
实际上是将共享资源转变成了临界资源。将并行的访问同一资源变成了并发的访问同一资源。