进程:正在运行的程序,是系统进行资源分配的基本单位。
线程:轻量级进程,是进程中的一条执行路径,也是CPU的基本调度单位。
进程与线程的关系:一个程序运行后至少有一个进程,进程由一个或者多个线程组成,进程间不能共享数据段地址,但是同进程的线程能共享数据段地址。
线程的组成部分包括CPU时间片,运行数据与逻辑代码:
cpu时间片:OS会给每个线程分配执行时间。
运行数据:
堆空间:存储线程使用的对象,多个线程可以共享堆中的对象。
栈空间:存储线程需要使用到的局部变量,每个线程都拥有独立的栈。
线程的特点:
1. 抢占式执行:可防止单一线程长时间占用cpu,效率高。
2.在单核cpu中,宏观上同时执行,微观上顺序执行。
创建线程的方式:
1. 继承Thread类,重写run方法
2.实现Runnable接口
3. jdk1.5之后,实现Callable接口
Callable接口:再JDK1.5以后加入,和Runnable接口类似,实现之后表示一个线程任务,但是它具有泛型返回值,可以声明异常。
public interface Callable<V> {
public V call() throws Execption;
}
Callable与Runnable的区别:
区别 | Callable | Runnable |
方法返回值 | call方法有返回值 | run方法无返回值 |
方法声明 | call方法有声明异常 | run方法没有异常 |
线程的同步与异步:
同步是指调用某个方法,必须等待该方法返回,才能往下走。
异步:调用某个方法,就像是一次通知,调用者通知该方法后,调用者继续往下执行,同时该方法也在和调用者竞争时间片,二者并发执行。
常用方法:
1.休眠:当前线程休眠millis毫秒。
public static void sleep(long millis);
2.放弃:当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
public static void yield();
3.加入:允许其他线程加入到当前线程中,并阻塞当前线程,直到加入的线程运行完毕,才会运行当前线程。这个是非静态方法,因此需要创建对象。
public final void join();
4.优先级:线程优先级为1-10,默认为5,优先级越高(值越大),表示获取cpu的机会越多。
设置优先级为i:线程对象.setPriority(i);
5.守护线程:线程有两类,一类是用户线程(前台线程),一类是守护线程(后台线程)。如果程序中的所有前台线程都执行完毕了,后台线程会自动结束。垃圾回收器就属于守护线程。
设置守护线程:线程对象.setDaemon(true);
守护线程demo如下,在主线程中将该线程设置为守护线程,主线程运行完毕后,守护线程也会结束:
守护线程:
package com.test5;
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();
}
}
}
}
主线程:
package com.test5;
public class TestDeamon {
public static void main(String[] args) {
DeamonThread deamonThread = new DeamonThread();
// 设置为守护线程
deamonThread.setDaemon(true);
deamonThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程===========" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
java多线程的安全问题:
临界资源是指共享资源(同一对象),一次仅允许一个线程使用,才可以保证其正确性。原子操作则是指不可分割的多步操作,被视为一个整体,其顺序与步骤不可打乱或者缺省。当多个线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
1.在应用程序中,用synchronized保证线程安全性的方法:
a. 同步代码块:
这里面的临界资源必须是互斥锁,具有唯一性,比如类对象等。
synchronized (临界资源){ // 对临界资源对象枷锁
// 代码(原子操作)
}
b.同步方法:
synchronized 返回值 方法名称(形参列表()) { // 对当前对象(this)加锁
// 代码(原子操作)
}
同步规则:
只有在调用同步代码块的方法或者使用同步方法时,才需要对象的锁标记,如果调用不包含同步代码块的方法或或者使用普通方法时,不需要锁标记,可直接调用。
JDK中线程安全的类:
StringBuffer,Vector,Hashtable(这些方法中都包含了synchronized修饰的同步方法)。
要注意的是,同步操作对程序性能有影响。
2.lock接口:
这个是JDK5以后加入的,它的效率比synchronized高,功能更强大,是显示定义,结构更灵活。
lock常用方法:
获取锁,如果锁被占用,则等待其释放(阻塞):void lock()
尝试获取锁,成功返回true,失败返回false,不阻塞:Boolean tryLock();
释放锁:unLock()
示例代码如下:
Lock l = ...;
l.lock();
try {
...;
} finally {
l.unlock();
};
a. 重入锁ReentrantLock的demo1:
调用类:
package com.test11;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyList {
// 创建锁
private Lock lock = new ReentrantLock();
private String[] str = {"I" , "liek", "", ""};
private int count = 2;
public void add (String string) {
lock.lock();
try {
str[count] = string;
count++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
public void prnintStr() {
for (String s : str) {
System.out.println(s);
}
}
}
主类:
package com.test11;
public class TestMylist {
public static void main(String[] args) {
final MyList myList = new MyList();
Runnable runnable1 = new Runnable() {
@Override
public void run() {
myList.add("haha");
}
};
Runnable runnable2 = new Runnable() {
@Override
public void run() {
myList.add("xixi");
}
};
Thread thread1 = new Thread(runnable1, "haha");
Thread thread2 = new Thread(runnable2, "xixi");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
myList.prnintStr();
}
}
b. ReentrantReadWriteLock读写锁:
支持一写多读,读写分离,读锁不互斥,写锁互斥,可分别配置读锁,写锁,可使得多个读操作并发执行。一般用在读数据线程较多的场合,在该场合中,在保证线程安全的前提下,读写锁比互斥锁效率更高。
互斥规则:
两个写线程:互斥,阻塞。
读线程与写线程:读阻塞写,写阻塞读,互斥。
两个读线程:不互斥。
demo如下:
读写类:
package com.test13;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyList {
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock rl = rwl.readLock();
ReentrantReadWriteLock.WriteLock wl = rwl.writeLock();
private int value;
// 读数据
public int getValue() {
rl.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return value;
} finally {
rl.unlock();
}
}
// 写数据
public void setValue(int value) {
wl.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.value = value;
} finally {
wl.unlock();
}
}
}
调用线程:
package com.test13;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestMyList {
public static void main(String[] args) {
final MyList myList = new MyList();
Runnable runSet = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "set value");
myList.setValue(1);
}
};
Runnable runGet = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "get value");
myList.getValue();
}
};
ExecutorService executorService = Executors.newFixedThreadPool(30);
for (int i = 0; i < 3; i++) {
executorService.submit(runSet);
}
for (int i = 0; i < 20; i++) {
executorService.submit(runGet);
}
executorService.shutdown();
}
}
继承Thread类,重写run方法创建线程的demo展示:
获取与修改线程相关信息:
1. 获取线程id与name:
a.在Thread的子类中调用this.getId与this.getName方法
b.使用Thread.currentThread().getId()与Thread.currentThread().getName()方法
2. 修改线程name:
a.使用线程对象的setName方法
b.使用线程子类的构造方法赋值
3. demo1:
a. 继承Thread,重写run方法:
package com.test1;
public class MyThread extends Thread {
// 创建无参构造方法
public MyThread() {
}
// 创建有参构造方法,将参数传给Thread
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 方法1:使用继承的Thread中的方法,获取线程的id与name
// System.out.println("线程id:"+this.getId() + "线程名称" + this.getName() +"hello " + i);
// 方法2:使用Thread.currentThread().getId()
System.out.println("线程id: " + Thread.currentThread().getId() + "线程name :" + Thread.currentThread().getName());
}
}
}
b.创建线程对象,启动线程:
package com.test1;
public class test {
public static void main(String[] args) {
// 设置线程对象名称的方法1
MyThread myThread = new MyThread("线程名称修改1");
// 设置线程对象名称的方法2
// myThread.setName("线程1");
myThread.start();
}
}
4. demo2:多个线程的运行:
a.继承Thread方法,运行run方法:
package com.test2;
public class TicketSell extends Thread {
private int ticket = 100;
public TicketSell() {
}
public TicketSell(String name) {
super(name);
}
@Override
public void run() {
while (true) {
if (ticket <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() +"卖了第" + ticket +"张票");
ticket--;
}
}
}
b.创建线程对象,启动线程:
package com.test2;
public class TestTicketSell {
public static void main(String[] args) {
TicketSell ticketSell1 = new TicketSell("售票处1");
TicketSell ticketSell2 = new TicketSell("售票处2");
TicketSell ticketSell3 = new TicketSell("售票处3");
TicketSell ticketSell4 = new TicketSell("售票处4");
ticketSell1.start();
ticketSell2.start();
ticketSell3.start();
ticketSell4.start();
}
}
实现Runnable接口的demo展示:
demo:
a.实现Runnable接口,覆盖run方法
package com.test3;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "hello " + i);
}
}
}
b.创建实现类对象,创建线程对象,并将实现类对象做为线程对象的参数,再调用start方法
package com.test3;
import com.test1.MyThread;
public class Test {
public static void main(String[] args) {
// 创建MyRunnable对象,表示线程要执行的功能
MyRunnable myRunnable = new MyRunnable();
// 创建线程对象
Thread thread1 = new Thread(myRunnable, "线程1");
// 启动线程对象
thread1.start();
}
}
如果a步骤中的Runnable接口的只想在当前环境中使用,不想单独建一个类来使用,通过匿名内部类的方式将上面a,b放到一起实现:
package com.test3;
import com.test1.MyThread;
public class Test {
public static void main(String[] args) {
// // 创建MyRunnable对象,表示线程要执行的功能
// MyRunnable myRunnable = new MyRunnable();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "你好 " + i);
}
}
};
// 创建线程对象
Thread thread1 = new Thread(runnable, "线程1");
// 启动线程对象
thread1.start();
}
}
Callable接口的demo展示:
package com.test9;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class callableDemo {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "计算1到10的乘积:");
int sum = 1;
for (int i = 1; i <= 10; i++) {
sum *= i;
}
return sum;
}
};
// 将callable转为一个可执行的任务
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask, "Thread");
thread.start();
// 获取线程返回的乘积结果
try {
Integer integer = futureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
该接口单独调用时,需要进行转换,因此,一般结合线程池使用。
线程通信:
等待方法:一个线程,在调用obj.wait()时,此线程会释放其拥有的所有锁标记,同时,此线程阻塞在等待队列中。
public final void wait()
public final void wati(long timeout)
通知方法:发送通知,唤醒线程。
public final void notify()
public final void notifyAll()
如下是一个存钱取钱的demo,有多个线程存钱,多个线程取钱:
package com.test7;
public class BankCard {
private double money;
private boolean flag; // true:进行取钱操作;false:进行存钱操作
public BankCard(double money) {
this.money = money;
this.flag = false;
}
// false:进行存钱操作
public synchronized void save(double m) { // this作为临界资源
while(flag) { // flag=true,进行的时取钱操作,存钱操作暂停
try {
this.wait(); // 进入等待队列,同时释放cpu等资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money += m;
System.out.println(Thread.currentThread().getName() + "存了" + m + "元, 余额是:" + money);
// 存钱操作完毕,可以取钱
flag = true;
// 唤醒取钱线程
this.notifyAll();
}
// true:进行取钱操作
public synchronized void take(double m) {
while (!flag) { // flag=false,进行存钱操作,取钱暂停
try {
this.wait(); // 进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money -= m;
System.out.println(Thread.currentThread().getName() + "取了" + m + "元, 余额是:" + money);
// 取钱操作完毕,可以存钱
flag = false;
// 唤醒存钱线程
this.notifyAll();
}
}
主函数:
package com.test7;
public class BankCardTest {
public static void main(String[] args) {
final BankCard card = new BankCard(10);
// 创建两个方法
Runnable saveMoney = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int m = 1000;
card.save(m);
}
}
};
Runnable takeMoney = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int m = 1000;
card.take(m);
}
}
};
// 创建线程
Thread threadA = new Thread(saveMoney, "haha_0");
Thread threadB = new Thread(takeMoney, "xixi_0");
Thread threadA_1 = new Thread(saveMoney, "haha_1");
Thread threadB_1 = new Thread(takeMoney, "xixi_1");
threadA.start();
threadB.start();
threadA_1.start();
threadB_1.start();
}
}
上面也是一个经典的生产者与消费者问题的处理方式:若干个生产者生产产品,若干个消费者消费产品。生产与消费的动作是并发执行,即存在一个空间,生产者在往这个空间存放物品的时候,消费者从这个空间搬走物。但是不能出现消费者到一个空的空间搬走物品,也不允许生产者往一个满的空间存放物品。
在多线程中,SimpleDateFormat存在线程安全问题,需要加所持能避免,可用DateTimeFormatter(以上从java 8 开始支持)。