第二十二章 并发编程
1.作业回顾
以指定的编码读取文件内容,并将读到的内容以指定的编码写文件,两种编码都配置到properties文件中。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
public class Day2201 {
public static void main(String[] args) {
String readCharset = null;
String writeCharset = null;
String readFilePath = null;
String writeFilePath = null;
InputStream is = Day2201.class.getResourceAsStream("/file.properties");
Properties props = new Properties();
try {
props.load(is);
readCharset = props.getProperty("readFilePath");
writeCharset = props.getProperty("writeCharset");
readFilePath = props.getProperty("readFilePath");
writeFilePath = props.getProperty("writeFilePath");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
isr = new InputStreamReader(new FileInputStream(readFilePath), readCharset);
osw = new OutputStreamWriter(new FileOutputStream(writeFilePath), writeCharset);
char[] chars = new char[256];
int len = 0;
while(-1 != (len = isr.read(chars))){
osw.write(chars, 0, len);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
osw.flush();
} catch (IOException e) {
e.printStackTrace();
}
try {
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2.进程和线程
在并发编程中,有两个基本的执行单元,进程和线程。
计算机运行时拥有很多活动的进程和线程,即使是单核系统也是如此。
进程可以理解为一个正在运行的软件,进程在运行时有独立的资源和内存空间。进程与进程之间不共享数据。进程的创建开销很大,因为要分配独立的资源和内存空间。
线程,例如音乐播放器在播放声音的同时可以滚动歌词,每个功能都由单独的一个线程来完成。 一个进程由很多线程组成,线程与线程之间共享进程的资源和内存空间。线程的创建开销很小,因为不需要分配独立的资源和内存空间。
计算机在运行时有很多线程,但是同一时刻只能有一个线程能运行。为了保证每个线程都能得到执行,操作系统将cpu的时间分为一个个细小的时间片,并给线程分配执行权。当一个线程获得cpu的执行权后,它可以在时间片内运行,当时间片过后,线程被剥夺执行权,操作系统开始进行下一轮执行权的分配。因为时间片很短,在一秒内每个线程都将得到多次执行机会,因此在用户的角度来看,感觉线程是并行执行的。
执行java程序需要使用java命令,java命令会运行java.exe程序,因此会创建一个进程。当java进程创建后,会创建一个线程来执行main方法的代码,这个线程叫做主线程。我们之前运行的java程序都是单线程的。
3.创建线程
在java中创建线程有两种方式:继承Thread类,实现Runnable接口。
3.1 继承Thread类来创建线程
class Mythread1 extends Thread{
//重写run方法
//当线程得到执行权,就会执行run方法中的代码
//当run方法中的代码执行完毕后,线程就死掉了
//线程可能run方法没有执行完毕,时间片就结束了
//线程会记录它执行的哪个代码了,当再次得到时间片,它会接着往下执行
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// System.out.println(this.getName() + ":" + (i + 1));
System.out.println(Thread.currentThread().getName() + ":" + (i + 1) );
}
}
}
public class Day2202 {
public static void main(String[] args) {
Thread t1 = new Mythread1();//创建一个线程对象t1,并不是真正的在创建线程
Thread t2 = new Mythread1();//创建一个线程对象t2,并不是真正的在创建线程
//start方法是启动线程,至于这个线程什么时候得到执行权,要看操作系统的调度
t1.start();//启动线程t1
t2.start();//启动线程t2
for (int i = 0; i < 1000; i++) {
//主线程
//得到当前线程,执行此代码的线程对象
Thread mainThread = Thread.currentThread();
//getName方法会返回线程的名字
System.out.println(mainThread.getName() + ":" + (i + 1));
}
}
}
3.2 实现Runnable接口来创建线程
class MyThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
//输出当前线程的名字
System.out.println(Thread.currentThread().getName() + ":" + (i + 1) );
}
}
}
public class Day2203 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread2());
t1.start();
Thread t2 = new Thread(new MyThread2());
t2.start();
}
}
4.线程类的方法
4.1 sleep方法
Thread.sleep(long millis)导致当前线程暂停执行指定的时间段。
public class Day2204 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i + 1));
try {
//让当前线程睡眠1000毫秒
//让出执行权,进行睡眠,睡眠结束后再参与cpu执行权的争夺
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.2 join方法
t1.join方法将导致当前线程等待t1执行完毕。
public class Day2205 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
//输出当前线程的名字
System.out.println(Thread.currentThread().getName() + ":" + (i + 1) );
}
}
});
//修改名称为t1
t1.setName("t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//输出当前线程的名字
System.out.println(Thread.currentThread().getName() + ":" + (i + 1) );
}
}
});
//修改名称为t2
t2.setName("t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//输出当前线程的名字
System.out.println(Thread.currentThread().getName() + ":" + (i + 1) );
}
}
});
//修改名称为t3
t3.setName("t3");
//t1和t2和主线程一起争夺时间片
t1.start();
t2.start();
try {
//让当前线程(主线程)暂停执行,等待t2执行结束主线程在恢复执行
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程恢复执行");
t3.start();
}
}
4.3 stop方法
t1.stop会导致线程t1立即终止执行。
public class Day2206 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i + 1) );
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止t1的执行
t1.stop();
//t1至少需要1000毫秒才可以执行完毕
//主线程睡100毫秒后t1还没执行完毕
}
}
4.4 interrupt方法
t1.interrupt会将设置t1线程为中断状态,但不会导致t1线程立即终止执行。 被中断的线程可以选择继续执行,也可以选择停止执行。
public class Day2207 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
//检查当前线程是否被打断
//isInterrupted方法返回中断状态
if (Thread.currentThread().isInterrupted()) {
System.out.println("线程被打断了");
break;
}else {
System.out.println(Thread.currentThread().getName() + ":" + (i + 1) );
}
}
}
});
t1.start();
try {
//让主线程先睡10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打断线程t1,设置t1的中断状态为true
t1.interrupt();
}
}
4.5 守护线程
java中的线程分为两种:守护线程和非守护线程。
守护线程是一种特殊的线程,当java虚拟机中所有的非守护线程都执行完毕后,守护线程就会终止执行。java中的垃圾回收器GC就是守护线程的典型例子。
平常使用的都是非守护线程。
public class Day2208 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i + 1));
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("守护线程在运行");
}
}
}, "t2");
//设置t2为守护线程
t2.setDaemon(true);
t1.start();//非守护线程执行完
t2.start();//守护线程就会终止执行
}
}
5.多线程并发问题
5.1原因
线程是并行执行的,当两个线程同时修改同一个数据,就会产生线程并发问题。
public class Account {
private int money = 0;
public void increase() {
money = money + 1;
}
public void decrease() {
money = money - 1;
}
@Override
public String toString() {
return "Account [money=" + money + "]";
}
}
public class IncreaseThread extends Thread{
private Account account;
public IncreaseThread(Account account) {
super();
this.account = account;
}
public void run() {
for (int i = 0; i < 10000; i++) {
account.increase();
}
}
}
public class DecreaseThread extends Thread{
private Account account;
public DecreaseThread(Account account) {
super();
this.account = account;
}
public void run() {
for (int i = 0; i < 10000; i++) {
account.decrease();
}
}
}
public class Day2209 {
public static void main(String[] args) {
Account account = new Account();
Thread t1 = new IncreaseThread(account);
Thread t2 = new DecreaseThread(account);
t1.start();
t2.start();
//主线程等待t1和t2执行结束
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(account);//有正有负
}
}
5.2 synchronized同步代码块
使用同步代码块,来保证增加1和减少1的操作都是原子的。
public class IncreaseThread extends Thread{
private Account account;
public IncreaseThread(Account account) {
super();
this.account = account;
}
public void run() {
for (int i = 0; i < 10000; i++) {
//synchronized是同步代码块
//要执行同步代码块,必须先获取account对象的锁
//每个对象都有一把锁
//同步锁同一时刻只能被一个线程获取
//没有获得锁的线程就会等待别的线程释放锁
//当线程在执行同步代码块时,时间片结束,线程会暂停执行,但是不会释放锁
//如果一个线程期望得到锁但是没有得到,那么它会放弃时间片
synchronized (account) {
account.increase();
}
}
}
}
public class DecreaseThread extends Thread{
private Account account;
public DecreaseThread(Account account) {
super();
this.account = account;
}
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (account) {
account.decrease();
}
}
}
}
6.练习
1,继承Thread类创建一个线程t1,实现Runnable接口创建一个线程t2。t1中使用for循环输出1000次自己的名字,每次输出后睡眠10毫秒,t2中使用for循环输出1000次自己的名字,每次输出后睡眠20毫秒。让主线程等待t1和t2都执行结束,主线程才恢复执行。