Java学习----多线程
如果说,java的集合是与计算机的内存有关。
则多线程是和计算机的CPU有关系。
而IO则是与计算机的硬盘有关系。
网络编程又是与网卡有关。
以上四部分也是JavaSE中最核心的内容。
1.什么是线程和进程
进程:进程是CPU资源分配的基本单位,程序是静止的,运行的程序我们叫作进程。
线程:又称为轻量级进程,是CPU的基本调度单位,一个进程一般由多个线程构成,这些线程彼此完成不同的工作,交替执行,这就是多线程。每个进程启动后,都会有一个主线程。
如:java的虚拟机就是一个进程,当我们启动一个java程序的时候,实际上是开了一个线程,其中java程序的的Main执行之后又是主线程。
进程之间不能共享数据段地址,但是同进程之间的线程是可以共享的。
人是一个进程,但是你可以一边上课一边打游戏,上课和打游戏是两个线程。
打开电脑的资源管理器,可以看到诸多正在执行的进程与线程数以及进程的PID等信息。
CPU通过进程的PID来管理线程。
2.线程的组成
线程最基本的组成部分
1.CPU时间片:OS会为每个线程分配执行时间。
2. 运行数据:堆空间:存储线程需要使用的对象,多个线程可以共享堆中的数据。
栈空间:存储线程使用的局部变量,每一个线程都拥有独立的栈。
3. 线程的逻辑代码。
线程的执行特点:
抢占式执行:效率高,可以防止一个线程长时间独占CPU。
在单核CPU上,宏观上同时执行,微观上顺序执行。
多核CPU是可以真正的实现多个线程并发执行。
3.java实现多线程
3.1线程的创建(继承Thread)
ok说了这么多,先看看,java里面一个线程的创建和运行。
JVM运行程序的时候会自动创建一个主线程(main)来执行main方法
public class MyThread extends Thread{
@Override
public void run(){
for(int i = 0;i < 5 ;i++){
System.out.println("这是子线程" + i);
}
}
}
主函数调用
public static void main(String[] args) {
MyThread myThread = new MyThread();
//启动子线程
myThread.start();
//此处不能直接myThread.run(),这样写话实际上还是主线程在执行
for (int i = 0;i < 5;i++){
System.out.println("这是主线程" + i);
}
}
执行之后的结果如下:
这里注意一点,由于现成的执行是抢占式执行,所以每次运行的结果都是不一样的
如此处第二次运行的结果与第一次运行的结果是不一样的。
这种情况下如果我们再添加一个线程myThread1的话,执行之后,子线程的输出会混在一起,无法区别是哪一个子线程的输出,这个时候我们可以通过调用,线程的getID()和getName()
方法,进行不同线程的输出区分。
public class MyThread extends Thread{
public MyThread(String threadName){
this.setName(threadName);
//super(threadName); 这样写 也是可以的
}
@Override
public void run(){
for(int i = 0;i < 5 ;i++){
//System.out.println("这是子线程" + i);
//此处这两个方法,是父类的方法
System.out.println(this.getId() + "******" + this.getName() + "子线程" + i);
//但是如果不继承Thread的话就不能使用上述方法
//通常会使用以下通用方法
System.out.println(Thread.currentThread().getId() + "---" + Thread.currentThread().getName());
}
}
}
主函数:
public static void main(String[] args) {
//创建现成的时候就设置现成的名字
MyThread myThread = new MyThread("thread1");
MyThread myThread1 = new MyThread("thread2");
//启动子线程
myThread.start();
myThread1.start();
//此处不能直接myThread.run(),这样写话实际上还是主线程在执行
for (int i = 0;i < 5;i++){
System.out.println("这是主线程" + i);
}
}
执行结果如下:
3.1.1 案列,实现窗口各卖100张票
代码很简单
public static void main(String[] args) {
TicketWin ticketWin1 = new TicketWin();
TicketWin ticketWin2 = new TicketWin();
TicketWin ticketWin3 = new TicketWin();
TicketWin ticketWin4 = new TicketWin();
ticketWin1.start();
ticketWin2.start();
ticketWin3.start();
ticketWin4.start();
}
TicketWin
public class TicketWin extends Thread{
//票
private int ticket = 100;
@Override
public void run(){
while(true){
if (ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - ticket) + "张票");
ticket --;
}
}
}
此示例内存占用示意图如下:
当ticketWin1
线程执行的时候。改变的tiket
就是属于ticketWin1
自己的ticket
,
3.2 线程的创建(实现Runnable接口)
Runnable
,含有run()
方法.
代码如下:
public class MyRunnable implements Runnable {
/**
* run()表示的是线程执行的功能
*/
@Override
public void run() {
for(int i = 0; i < 50; i ++ ){
System.out.println(Thread.currentThread().getName() + "---" + "this is a thread" + i);
}
}
}
主函数代码:
public static void main(String[] args) {
//创建一个可运行对象(封装了线程执行的功能),但是执行的时候需要,交给线程来执行
MyRunnable myRunnable = new MyRunnable();
//创建线程对象
Thread thread = new Thread(myRunnable);
//启动线程
thread.start();
}
执行结果:
3.2.1 案例,四个窗口,共卖100张票
100张票涉及到资源共享问题。涉及到线程安全问题。
不考虑线程安全问题的时候,代码如下:
Ticket
public class Ticket implements Runnable{
//100张票
private int num = 100;
//卖票,用run方法实现卖票功能
@Override
public void run(){
while (true){
if(num <= 0){
break;
}
System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - num) + "张票");
num --;
}
}
}
主函数
public static void main(String[] args) {
//1.创建票(也是可执行对象)
Ticket ticket = new Ticket();
//2.创建卖票的窗口
Thread win1 = new Thread(ticket);
Thread win2 = new Thread(ticket);
Thread win3 = new Thread(ticket);
Thread win4 = new Thread(ticket);
//3.四个窗口用的是同一个票的资源
win1.start();
win2.start();
win3.start();
win4.start();
}
由于没有考虑线程的安全性,所以会出现,多个窗口同时卖一张票的情况,执行结果:
很明显,上述线程0和线程3同时卖了第14张票。考虑线程安全的情况之后会给出。
先看一下,此时的内存示意图。
这时候,如果是我们启动了win1
线程,则,有如下示意图。
同理,无论是win2
,win3
,win4
最后变得ticketnum
都是来自于同一个ticket
的
3.2.2 案例,你和你gf共用一张银行卡,你往卡里存钱,你女朋友花钱。
上述,案例中,run方法是可以放在ticket里面,因为上述的案例中,只有一种卖票操作,此案例中有,取钱和存钱两种不同的操作,所以就设置两种行为来分别表征。
代码如下:BankCard
类
public class BankCard {
private double money = 0;
public BankCard(){
}
public BankCard(double money) {
this.money = money;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
存钱类AddMoney
public class AddMoney implements Runnable {
private BankCard card;
public AddMoney(BankCard bankCard){
this.card = bankCard;
}
//执行存钱功能。
@Override
public void run() {
for (int i = 0;i < 10; i++){
card.setMoney(card.getMoney() + 1000);
System.out.println(Thread.currentThread().getName() + "存了1000元,余额是" + card.getMoney());
}
}
取钱类:SubMoney
public class SubMoney implements Runnable{
private BankCard card;
public SubMoney() {
}
public SubMoney(BankCard bankCard) {
this.card = bankCard;
}
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(this.card.getMoney() >= 1000){
card.setMoney(card.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() + "取钱,取了一千,余额是" + card.getMoney());
}
else {
System.out.println("赶紧存钱");
i--; //没取到钱的话不算,保证钱一定能够花完
}
}
}
主函数:main()
public static void main(String[] args) {
//银行卡
BankCard bankCard = new BankCard();
//存钱功能
AddMoney addMoney = new AddMoney(bankCard);
//取钱功能
SubMoney subMoney = new SubMoney(bankCard);
//线程来执行功能
//你自己要存钱
Thread yourself = new Thread(addMoney, "你自己");
//女朋友取钱
Thread yourgirl = new Thread(subMoney, "女朋友");
//启动线程
yourself.start();
yourgirl.start();
}
由于目前没有考虑线程安全的问题,所以会出现很多不合理的情况。运行部分结果如下所示。
上述代码的写法都比较基础,其实可以使用匿名内部类的写法。代码如下:
public static void main(String[] args) {
//创建银行卡
BankCard bankCard = new BankCard();
//存钱
Runnable addMoney = new Runnable() {
private BankCard card = bankCard;
@Override
public void run() {
for(int i = 0; i < 10; i++ ){
this.card.setMoney(this.card.getMoney() + 1000);
System.out.println(Thread.currentThread().getName() + "存钱成功,余额是" + this.card.getMoney());
}
}
};
//取钱
Runnable subMoney = new Runnable() {
private BankCard card = bankCard;
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(this.card.getMoney() >= 1000){
this.card.setMoney(this.card.getMoney() - 1000 );
System.out.println(Thread.currentThread().getName() + "取钱成功,余额是" + this.card.getMoney());
}
else {
System.out.println("余额不足,赶紧打钱");
i--;
}
}
}
};
//执行线程
new Thread(addMoney, "yourself").start();
new Thread(subMoney, "yourgirl").start();
}
执行结果同样是乱七八糟的。
由于线程是抢占式执行,在写程序的时候,可以多运行几次试试,体验一把每次都是新结果的新奇。
4.线程的状态
现在,结合上述内容说一下线程的诸多状态以及状态之间的转换。
调用Thread
的一个叫getState()
的方法:
去查看State
这是一个枚举类型:内含有所有状态。并含有所有状态的描述,有兴趣的可以去看。
1. NEW
2. RUNNABLE
3. BLOCKED
4. WAITING
5. TIMED_WAITING
6. TERMINATED
有了线程基本状态变化的基础之后,接下来看一下java里面,提供的给线程执行的一些常见方法。
4.1 常见的线程的方法
4.1.1 休眠sleep(long millis)
当前线程主动休眠millis秒,此期间将CPU释放,并不再争抢。
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0;i < 50;i++){
System.out.println(Thread.currentThread().getName() + "--" + i);
try {
Thread.sleep(1000); //休眠一会
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行的时候是一个一个慢慢的出现。
4.1.2yield()主动释放CPU
但是释放了CPU之后,还会参与争抢,它只会把CPU让给优先级大于等于它的线程。
public class YieldThread extends Thread{
@Override
public void run(){
for (int i = 0;i < 50;i++){
System.out.println(Thread.currentThread().getName() + "--" + i);
Thread.yield();
}
}
}
这个具体执行也看不出效果。
4.1.3 join()
允许其他线程加入到当前线程的执行,当前的线程会阻塞,直到加入的线程执行结束。
代码:
public class JoinThread extends Thread {
@Override
public void run(){
for(int i = 0;i < 50;i++){
System.out.println(Thread.currentThread().getName() + "--" + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
主函数:
public static void main(String[] args) {
JoinThread joinThread = new JoinThread();
joinThread.start();
try {
//加入线程,把joinThread加入到当前线程,
// 等待joinThread执行完成之后,当前进程再次进行执行。
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i = 0;i < 50; i++ ){
System.out.println("这是主线程----*" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果是:
可见,子线程全都执行完之后才会执行主线程中的内容。
4.1.4 setPriority设置线程优先级
优先级为1-10,创建线程不设置的话,默认优先级是5。
代码:
public class PriorityThread extends Thread {
public PriorityThread(String name) {
super(name);
}
@Override
public void run(){
for(int i = 0; i < 50; i++ ){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
主函数:
public static void main(String[] args) {
PriorityThread p1 = new PriorityThread("thread1");
PriorityThread p2 = new PriorityThread("thread2");
PriorityThread p3 = new PriorityThread("thread3");
p1.setPriority(1);
p3.setPriority(10);
p1.start();
p2.start();
p3.start();
}
设置了优先级之后,p1抢到CPU的几率是最小的,所以p1最晚执行完的几率会大,但是也不是绝对的。
4.1.5 interrupt()打断线程
会打断线程的执行,下面是例子
InterruptThread
类
public class InterruptThread extends Thread{
@Override
public void run(){
System.out.println("子线程开始执行了");
try {
System.out.println("开始休眠");
Thread.sleep(2000);
System.out.println("休眠结束");
}
catch (InterruptedException e) {
System.out.println("休眠的两秒内被打断了");
}
System.out.println("子线程执行结束了");
}
}
主函数:
public static void main(String[] args) throws IOException {
InterruptThread interruptThread = new InterruptThread();
interruptThread.start();
System.out.println("20秒之内,输入任意字符,打断子线程");
Scanner input = new Scanner(System.in);
System.in.read();//程序执行到这里之后,如果不输入字符就会停止继续往下执行
interruptThread.interrupt();
}
如果不输入任何字符的话,子线程会顺利的休眠20S然后执行结束(主线程不会)
如果此时输入一个任意字符,则子线程还没有休眠完成就会被打断。
4.1.6守护线程
线程有两类,分别是:用户线程(前台线程)和守护线程(后台线程)
如果程序中所有的前台线程都执行完了,后台线程就会自动结束。
Java中垃圾回收器线程属于守护线程。
java中可以通过setDaemon(true)
设置为守护线程。
示例代码:
public class DeamonThread extends Thread{
@Override
public void run(){
for(int i = 0;i < 50;i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
主函数代码:
public static void main(String[] args) {
DeamonThread deamonThread = new DeamonThread();
//设置为守护线程
deamonThread.setDaemon(true);
deamonThread.start();
for(int i = 0;i < 50; i++ ){
System.out.println("这是主线程----*" + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
我们可以看到,子线程虽然也是循环50次,但是它却在执行完20次的时候就停止了,主要是因为:当前的子线程为后台线程,而后台线程存在的意义是为用户线程(前台线程)服务。所以当主线程执行结束之后,子线程也结束了。
5.线程同步
上述事例中,当多个线程同时执行的时候,由于线程的执行是抢占式执行,所以会发生很多我们不想要的结果和情况。再比如,如果两个线程同时访问一个数组,一个用于存数据,一个用于取数据,那么取数据的线程如果执行在存数据的线程之前,就会发生错误。这是我们不想看到的情况。
以上问题,都可以通过给线程所使用到的临界资源上锁来解决,以下就是线程的同步问题。
上锁在操作系统上来说是一门“艺术”了,但是在java的实现中,只是依托于synchronized
的几行代码(开发者牛!!!!)。
5.1 同步代码块
5.1.1 重写四个窗口卖100张票案例
分析之前的代码,主要原因是:
当第一个窗口卖了第一百张票的时候,i--
还没有执行,CPU给予当前线程的时间片已经到了。第二个窗口马上抢占了CPU,第二个窗口这里发现 ,第一百张票还在,于是接着卖第一百张票…
1.找到发生冲突的地方加锁,就这个案例来说是while里面的代码。
代码:Ticket
类
利用Object加锁
public class Ticket implements Runnable{
private int ticket = 100;
private Object lock = new Object();
public Ticket() {
}
@Override
public void run() {
while (true){
synchronized (lock){
if (ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket);
ticket--;
}
}
}
}
2.对当前对象ticket的引用加锁:
public class Ticket implements Runnable{
private int ticket = 100;
//private Object lock = new Object();
public Ticket() {
}
@Override
public void run() {
while (true){
synchronized (this){//指当前对象的引用
if (ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket);
ticket--;
}
}
}
}
主函数:
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
Thread t4 = new Thread(ticket,"窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
执行结果,发现没有冲突发生了。
5.2 同步方法
如果一整个方法的代码都需要加锁,则直接对整个方法上锁。
- 如果方法是实例方法则,锁是this
- 如果是静态方法,锁是
类名.class
代码
public class Ticket implements Runnable{
private int ticket = 100;
// private Object lock = new Object();
public Ticket() {
}
@Override
public void run() {
while (true){
if (!sellticket()){
break;
}
}
}
public synchronized boolean sellticket(){ //这个时候锁是this
if (ticket <= 0){
return false;
}
System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket);
ticket--;
return true;
}
}
5.3 补一点死锁的知识
通俗一点解释,就是线程1手里有A资源但是在等B资源,线程2手里有B资源但是在等A资源,导致两个线程谁也无法执行完,谁呀无法释放资源,滞留在原地
例子,假如说,两个人去吃饭,A拿着第一根筷子,B拿着第二根筷子,谁都吃不了饭。
代码示例:
筷子类Chosticks
public class Chopsticks {
protected static Object chopstick1 = new Object();
protected static Object chopstick2 = new Object();
}
第一个人:
public class Person1 extends Thread{
@Override
public void run(){
synchronized(Chopsticks.chopstick1){
System.out.println(Thread.currentThread().getName() + "得到了第一根筷子");
synchronized(Chopsticks.chopstick2){
System.out.println(Thread.currentThread().getName() + "得到了第二跟筷子");
System.out.println(Thread.currentThread().getName() + "可以吃饭了");
}
}
}
}
第二个人:
public class Person2 extends Thread{
@Override
public void run(){
synchronized(Chopsticks.chopstick2){
System.out.println(Thread.currentThread().getName() + "得到了第二根筷子");
synchronized(Chopsticks.chopstick1){
System.out.println(Thread.currentThread().getName() + "得到了第一根筷子");
System.out.println(Thread.currentThread().getName() + "可以吃饭了");
}
}
}
主函数main:
public static void main(String[] args) {
Person1 person1 = new Person1();
Person2 person2 = new Person2();
person1.start();
person2.start();
}
执行结果:会陷入死锁状态,一直相互等待
6.总结
以上全都是一些基础的操作,后续的一些复杂操作,会在下一篇帖子写出,写CSDN主要是为了自己记录笔记,这又是一个很方便的平台,出现错误请务必及时指正。内容有的是看视频有的是看书得到的,有时候也会盗几个图。
ps:不知道是不是篇幅太长的原因,这篇文章的后半段打字确实卡的离谱。于是决定后续内容新开一篇。