文章目录
- 引言
- 基础知识
- Synchronized关键字
- 使用方式
- 用于同步方法
- 针对同步块的方法
- 静态方法使用
- 原理解析
- Volatile
- 使用方式
- 实现原理
- final关键字
- 编程练习(synchronized就能实现)
- 双线程轮流打印1-100
- 个人实现
- 参考实现
- 三线程顺序打出1-100
- 个人实现
- 参考实现
- 三个线程分别打印1,2,3,然后执行10次
- 个人实现
- 参考实现
- 线程交叉打印12A34B56C
- 个人实现
- 参考实现
- 交替打印大小写字母
- 个人实现
- 参考实现
- 模仿购票系统
- 个人实现
- 参考实现
- 总结
引言
- 今天面试字节,让我用Java实现一个多线程编程,控制三个线程完成按照顺序输出1、2、3,给我整郁闷了,我就是背了,看了,但是就是没有写过!今天一定要花时间,把这些东西全部都写一遍,做一遍,以面试题为导向!
- 这篇文章主要是先补充基础知识,然后针对性的找一些编程题目进行训练。
- 这里关于JMM并没有深究,单纯是从使用的角度来看的,所以happens-before并没有展开讲,后续有时间在好好讲讲!
基础知识
Java内存模型JMM
- JMM决定了一个线程对共享变量写入时,能对另一个线程可见。
- 线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程读写共享变量的副本。
Synchronized关键字
使用方式
- 对于普通方法而言,锁是当前实例对象
- 对于同步方法块,锁是Synchronized括号里面的指定的对象(可以是this,或者其他传入的实例对象)
- 对于静态同步方法,锁是当前类的Class对象
用于同步方法
- 当前的实例对象
public class Main implements Runnable{
@Override
public void run(){
method();
}
// 这里使用sychronzied直接修饰方法,使用这个实例的所有线程都是的需要获得这个锁
public synchronized void method(){
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Thread " + Thread.currentThread().getName() + " is finished");
}
public static void main(String[] args) {
// 定义一个对象实例,所有使用这个实例的线程都是使用一个对象锁
Main main = new Main();
Thread t1 = new Thread(main);
Thread t2 = new Thread(main);
t1.start();
t2.start();
}
}
上述程序共用一个main实例,然后是顺序执行,第一个线程执行完了,然后在执行第二个线程
分析
- synchronized修饰一个类非静态方法,默认是使用这个类的具体实例
- t1和t2是使用同一个类的实例main,两个线程会进行锁的竞争,获得锁之后才能顺利执行
针对同步块的方法
- synchronized需要传入一个对应的实例,这里是使用了这个代码块所在类的一个实例对象。
public class Main implements Runnable{
private Object lock = new Object();
@Override
public void run(){
method();
}
public void method(){
// synchronized (this){
synchronized (lock){
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Thread " + Thread.currentThread().getName() + " is finished");
}
}
public static void main(String[] args) {
// 定义一个对象实例,所有使用这个实例的线程都是使用一个对象锁
Main main = new Main();
Thread t1 = new Thread(main);
Thread t2 = new Thread(main);
t1.start();
t2.start();
}
}
上述程序和synchronized修饰方法的效果是一致的,不顾需要传入一个单独的对象,当然也可以传入对应的this,也就是当前对象本身作为锁
分析
- 上述同步代码块中,synchronized传入的对象是lock,也可传入当前对象this
- 两个使用同一个实例创建线程t1和t2,会竞争获取对象锁lock,执行成功后,会自动解锁,运行下一个线程。
静态方法使用
- 对于静态同步方法,使用的对象锁是当前的类Class对象
- 和同步方法差不多,只不过方法必须要用static进行修饰
public class Main implements Runnable{
// private Object lock = new Object();
@Override
public void run(){
method();
}
public static synchronized void method(){
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Thread " + Thread.currentThread().getName() + " is finished");
}
public static void main(String[] args) {
// 定义一个对象实例,所有使用这个实例的线程都是使用一个对象锁
Main main1 = new Main();
Main main2 = new Main();
Thread t1 = new Thread(main1);
Thread t2 = new Thread(main2);
t1.start();
t2.start();
}
}
分析
- 如果synchronized是修饰静态方法static的话,这个锁就是当前类的Class对象,然后只要是使用这个类的实例创建对象的线程,都是必须要竞争这个锁,实现线程同步。
- 不像前两种方法,必须要使用同样的实例对象,创建线程。
原理解析
加锁原理分析
- synchronized是基于监视器monitor实现
- 当一个线程尝试访问一个被synchronized修饰的对象时,一定要先获取对象的monitor监视器
- 获取成功,执行monitor.enter方法,执行对应操作,执行完毕之后,执行monitor.exit方法
- 获取失败,执行monitor.exit方法,进入等待队列SynchronizedQueue方法
- 当一个线程尝试访问一个被synchronized修饰的对象时,一定要先获取对象的monitor监视器
可重入的实现原理
- 拥有一个monitor计数器,当线程获取该对象锁之后,计数器会加1,不会出现问题,释放锁会减1,直到计数器为0,才能彻底释放锁。
保证同一个对象中的多个被synchonized方法能够顺序执行
public class Main implements Runnable{
// private Object lock = new Object();
@Override
public void run(){
method1();
method2();
method3();
}
public static synchronized void method1(){
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
System.out.println("method 1 running");
System.out.println("Thread " + Thread.currentThread().getName() + " is finished");
}
public static synchronized void method2(){
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
System.out.println("method 2 running");
System.out.println("Thread " + Thread.currentThread().getName() + " is finished");
}
public static synchronized void method3(){
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
System.out.println("method 3 running");
System.out.println("Thread " + Thread.currentThread().getName() + " is finished");
}
public static void main(String[] args) {
// 定义一个对象实例,所有使用这个实例的线程都是使用一个对象锁
Main main1 = new Main();
Main main2 = new Main();
Thread t1 = new Thread(main1);
Thread t2 = new Thread(main2);
t1.start();
t2.start();
}
}
通过下述代码可以看出,每一个线程都是先执行方法1,方法2,方法3的顺序执行的
锁的内存语义
- 当线程释放锁的时候,JMM会把该线程对应的本地内存中的共享变量,刷新到主内存中。
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效
锁的升级
-
偏向锁
- 不需要加锁解锁,默认只有一个人用
- 没有加锁解锁消耗
-
轻量级锁
- 轻量级锁的状态下,需要通过自旋+CAS,实现抢锁,使用拷贝的方式,将对应的LR锁记录复制到Mark Word中。
- 会自旋
-
重量级锁
- 轻量级锁自旋获取失败之后,就会升级为重量级锁
- 线程加锁失败后,会进入阻塞状态,等待唤醒。
- 直接阻塞
Volatile
使用方式
- 如果一个字段被声明为volatile,就能使所有线程都能看到这个变量的值
未使用volatile的线程同步
import java.util.concurrent.TimeUnit;
public class Main{
public static void main(String[] args) {
new Thread("Thread A"){
@Override
public void run(){
while(flag){
}
System.out.println("Thread A is finished");
}
}.start();
try{
TimeUnit.SECONDS.sleep(1);
System.out.println("Thread main wait for a second");
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Thread main change the flag");
flag = false;
}
}
上述程序会一直执行,不会退出,因为在Thread A的本地内存中,flag的值一直是true,main线程修改并没有同步到他的线程中
使用volatile的线程同步
import java.util.concurrent.TimeUnit;
public class Main{
public static volatile boolean flag = true;
public static void main(String[] args) {
new Thread("Thread A"){
@Override
public void run(){
while(flag){
}
System.out.println("Thread A is finished");
}
}.start();
try{
TimeUnit.SECONDS.sleep(1);
System.out.println("Thread main wait for a second");
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Thread main change the flag");
flag = false;
}
}
使用volatile之后,main修改flag之后,Thread A能够立刻看见,然后退出执行
实现原理
内存JMM模型不同线程的本地存储空间
- 因为不同线程都有一个本地的内存空间,对于共享变量也会复制一份到本地内存中。
- 程序出现一直运行的状态是因为本地内存的共享变量没有和主内存中的变量进行同步,即使共享内存修改了,但是本地内存还是原来那个值。
volatile写-读的内存语义
- 当写一个volatile变量时
- JMM会立刻将本地内存中的共享变量刷新到主内存中。
- 当读一个volatile修饰的变量时
- JMM会将本地内存的变量设置为无效,线程只能从主内存中读取对应的共享变量。
volatile禁止重排序
- 为了性能优化,JMM会在不改变正确语义的前提下,允许编译器和处理器对指令进行重新排序
- volatile会在对应的位置加上内存屏障,禁止指令重排。
至于怎么禁止重排的,没有必要深究,问到了就是不会,总不至于让我写一下!而且有没有需要编码的!
原子性相关
- volatile不能保证原子性,但是对于long和double的操作会保证原子性
- volatile会保证先行关系,写操作会在读操作之前执行
final关键字
- 修改类,这个类不能被继承
- 修饰方法,这个方法不能被子类重写
- 修饰参数,无法更改参数引用所指向的对象
- 修饰变量,这个变量是常量,编译期就确定,后期无法修改(可能不够严谨,但是应付面试足够了!)
- static final:该字段只占据一段不能改变的存储空间,必须在定义的时候就进行赋值,否则编译器不能通过。
final禁用指令重排
感觉其他的ReentranLock等有点多了,今天有点腻了,看不下去了,明天再看,下面的题目也是使用synchronized就能实现的
编程练习(synchronized就能实现)
双线程轮流打印1-100
- 一个线程打印奇数,一个线程打印偶数,你写一个程序,控制这两个线程在控制台输出1到100
个人实现
import java.util.concurrent.TimeUnit;
public class Main{
public static volatile int current = 1;
public static final Object lock = new Object();
static class Solution implements Runnable{
int outputType = 0;
public Solution(int num){
outputType = num;
}
@Override
public void run(){
synchronized (lock){
while(current < 100){
try {
while(current % 2 != outputType) lock.wait();
System.out.println(current);
current++;
lock.notifyAll();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
Solution odd = new Solution(1);
Solution even = new Solution(0);
Thread t1 = new Thread(odd);
Thread t2 = new Thread(even);
t1.start();
t2.start();
}
}
总结
- 本来想按照一个特定的步骤开始写,先写一个循环输出的线程,然后在进行协调,结果发现,还是不断修改,改回来最初的样子,也算是实现了。
参考实现
import java.util.concurrent.TimeUnit;
public class Main{
private static int currentNum = 1; //当前需要打印的数字
public static final Object lock = new Object();
static class Solution implements Runnable{
boolean isOdd ;
public Solution(boolean isOdd){
this.isOdd = isOdd; // 是否是奇数
}
@Override
public void run(){
while(currentNum < 10){
synchronized (lock){
// 判定阻塞情况,自身是奇数,然后需要输出的数字是偶数
// 自身是偶数,然后需要输出的数字是奇数
while((isOdd && currentNum % 2 == 0) || (!isOdd && currentNum % 2 == 1)){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// 进入当前状态后开始打印
System.out.println("Thread odd or even:" + isOdd +" print:" + currentNum ++);
lock.notifyAll();
}
}
}
}
public static void main(String[] args) {
Solution odd = new Solution(true);
Solution even = new Solution(false);
Thread t1 = new Thread(odd);
Thread t2 = new Thread(even);
t1.start();
t2.start();
}
}
总结
- 这里就没有必要使用volatile,因为每一次加锁和解锁,线程都会更新本地内存中的共享变量。
- 线程之间的配合需要使用wait和notify两个函数进行相互配合。
- 这个东西写过了就直到,没写过,就是写不出来。
相当于遍历currentNum,从1到100,然后轮流由两个对应奇数和偶数的线程输出
三线程顺序打出1-100
- 需要实现三个线程交替打印输出,一直输出到100。
个人实现
- 这里就是分为三个部分,第一个线程负责3n+1,第二个线程负责打印3n+2,第三个负责打印3n+3,控制一下输出的次序就行!
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object();
public static int count = 1;
static class PrintThread implements Runnable{
// 0: 3n,1: 3n+1,2: 3n+2
private int threadNum ;
private int threadName;
PrintThread(int threadNum,int name){
this.threadNum = threadNum;
this.threadName = name;
}
@Override
public void run(){
while(count < 100) {
synchronized (lock) {
// judge whether the thread is de right one
try {
while (count % 3 != threadNum) {
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// print out the current
System.out.println("Thread - " + threadName + " : " + count);
count++;
// wake up other threads
lock.notifyAll();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new PrintThread(1,1));
Thread t2 = new Thread(new PrintThread(2,2));
Thread t3 = new Thread(new PrintThread(0,3));
t1.start();
t2.start();
t3.start();
}
}
整体思路是一致的,就是改一下部分东西
参考实现
- 这里的思路和我的一致,不过用到了匿名内部类的方法。
- 在最后输出加了一个判定,保证最终的输出100以及以内的数字
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object(); // 同步的锁对象
public static int count = 1; // 要打印的数字
private static int turn = 1;// 控制哪个线程输出
public static void printNum(int offset){
while(count < 100){
synchronized (lock){
// judge whether should print the result
while(turn % 3 != offset){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// judge to print the result
if(count <= 100){
System.out.println(count);
count++;
turn = (turn + 1) % 3;
lock.notifyAll();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(()->printNum(1));
Thread t2 = new Thread(()->printNum(2));
Thread t3 = new Thread(()->printNum(0));
t1.start();
t2.start();
t3.start();
}
}
- 感觉这个代码更加繁琐,不过基本思路都是一样,练习一下完事了!
三个线程分别打印1,2,3,然后执行10次
- 线程A负责循环打印输出1,线程B负责循环打印输出2,线程C负责循环打印输出3
这题是字节面试的原题
个人实现
- 跟之前的题目很相似,控制三个线程各自输出对应的值,然后控制一下输出的总的次数就行了
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object(); // 同步的锁对象
public static int count = 1; // 要打印的数字
private static int turn = 1;// 控制哪个线程输出
public static void printNum(int offset,int num){
while(count < 30){
synchronized (lock){
// judge whether should print the result
while(turn % 3 != offset){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// judge to print the result
if(count <= 30){
System.out.println(num);
count++;
turn = (turn + 1) % 3;
lock.notifyAll();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(()->printNum(1,1));
Thread t2 = new Thread(()->printNum(2,2));
Thread t3 = new Thread(()->printNum(0,3));
t1.start();
t2.start();
t3.start();
}
}
- 没咋改,基本上就是增加了一个变量num,进行输出即可。
参考实现
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object(); // 同步的锁对象
public static int count = 1; // 要打印的数字
private static int turn = 1;// 控制哪个线程输出
public static void printNum(int offset,int num){
for(int i = 1;i <= 10;i ++){
// control each thead run 10 times
synchronized (lock){
// judge whether should print the result
while(turn % 3 != offset){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// judge to print the result
if(count <= 30){
System.out.println(num);
count++;
turn = (turn + 1) % 3;
lock.notifyAll();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(()->printNum(1,1));
Thread t2 = new Thread(()->printNum(2,2));
Thread t3 = new Thread(()->printNum(0,3));
t1.start();
t2.start();
t3.start();
}
}
- 他就是加了一个for循环,进行精确控制。
线程交叉打印12A34B56C
- 创建两个线程,一个线程专门用来的打印数字,一个线程专门用来打印字母,然后写一个多线程程序,控制输出如下顺序12A34B45C…
个人实现
- 还是两个线程,主要是打印输出的问题,
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object(); // 同步的锁对象
public static int count = 1; // 要打印的数字
private static int turn = 1;// 控制哪个线程输出
public static String changeStr(String str){
StringBuilder res = new StringBuilder("");
for(int i = 0;i < str.length();i ++){
res.append((char) (str.charAt(i) + str.length()));
}
return res.toString();
}
public static void printNum(String output,int offset){
for(int i = 1;i <= 3;i ++){
// control each thead run 10 times
synchronized (lock){
// judge whether should print the result
while(turn % 2 != offset){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// judge to print the result
System.out.print(output);
output = changeStr(output);
count++;
turn = (turn + 1) % 2;
lock.notifyAll();
}
}
}
public static void main(String[] args) {
Thread threadNum = new Thread(()->printNum("12",1));
Thread threadChar = new Thread(()->printNum("A",0));
threadNum.start();
threadChar.start();
}
}
总结
- 通过trun来控制不同线程之间的输出打印顺序,每一个线程一个偏移号offset
- 找到迭代输出的规律,在统一的实现方法中进行修改!
参考实现
- 这里就写的很明确,写了两个线程,各自负责各自的输出,然后通过一个标志位进行控制,具体实现如下
import java.awt.*;
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object(); // 同步的锁对象
public static boolean printNum = true; // 要打印的数字
public static void main(String[] args) {
Thread threadNum = new Thread(()-> {
for(int i = 1;i <= 26;i ++){
synchronized(lock){
while(!printNum){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.print(i);
System.out.print(++i);
printNum = false;
lock.notifyAll();
}
}
});
Thread threadChar = new Thread(()-> {
for(char i = 'A';i <= 'Z';i ++){
synchronized(lock){
while(printNum){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.print(i);
printNum = true;
lock.notifyAll();
}
}
});
threadNum.start();
threadChar.start();
}
}
总结
- 可以各个线程各自实现自己的输出,通过一个多线程共享的变量协调线程之间的输出顺序。
交替打印大小写字母
- 创建两个线程,第一个线程打印大写字母,第二个线程打印小写字母,然后让他们交替输出
个人实现
- 这个跟上面很像,就是一个输出数字一个输出字母的。
import java.awt.*;
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object(); // 同步的锁对象
public static boolean printBig = true; // 要打印的数字
public static void main(String[] args) {
Thread threadBig = new Thread(()-> {
for(char i = 'a';i <= 'z';i ++){
synchronized(lock){
while(printBig){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.print(i);
printBig = true;
lock.notifyAll();
}
}
});
Thread threadSmall = new Thread(()-> {
for(char i = 'A';i <= 'Z';i ++){
synchronized(lock){
while(!printBig){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.print(i);
printBig = false;
lock.notifyAll();
}
}
});
threadBig.start();
threadSmall.start();
}
}
总结
- 其实这类题目还是蛮好做的,就是记住一个范式就行了!
参考实现
-
这里使用使用了一个函数类来实现的,通过toLowerCase来实现小写的转换
-
这里就不写了,基本上大差不差!
模仿购票系统
- 模仿购票系统,目前有500张票,同时有4个窗口,模拟购票过程,打印购票结果,窗口1购买一张票,剩余499张票。
个人实现
- 4 个窗口,对应4个线程
- 票总数是500,这个应该对应的是需要互斥访问并且修改的变量,并不需要wait和notifyAll,只需要使用synchronized关键字就行,控制互斥访问。
import java.awt.*;
class Main{
// 实现三个线程交替输出1到100
// define the lock Object
public static final Object lock = new Object(); // 同步的锁对象
public static boolean printBig = true; // 要打印的数字
static class TicketSystem implements Runnable{
static int ticketNum = 500;
public synchronized void buyTicket(){
System.out.println(Thread.currentThread().getName() + " buy ticket " + ticketNum-- + " left " + ticketNum);
}
@Override
public void run(){
while(ticketNum > 0){
buyTicket();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new TicketSystem(), "1");
Thread t2 = new Thread(new TicketSystem(), "2");
Thread t3 = new Thread(new TicketSystem(), "3");
Thread t4 = new Thread(new TicketSystem(), "4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
参考实现
- 它实现的更加具体,还模拟了购票之后的随机操作,确实更加具体,还模拟了购买票之后的随机操作。
- 该方法使用的同步代码块实现的
import java.awt.*;
class Main{
// 实现三个线程交替输出1到100
public static final int TICKET_NUM = 500;
public static int resTicket = TICKET_NUM;
public static final Object lock = new Object();
static class TicketSystem implements Runnable{
public void buyTicket(){
System.out.println("Windows:" + Thread.currentThread().getName() +
" buy ticket " + 1 + " left " + resTicket--);
}
@Override
public void run(){
while(resTicket > 0) {
synchronized (lock) {
if (resTicket > 0) buyTicket();
else System.out.println("Windows:" + Thread.currentThread().getName() + " sold out");
}
}
try {
Thread.sleep((long) (Math.random() * 10));
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new TicketSystem(), "1");
Thread t2 = new Thread(new TicketSystem(), "2");
Thread t3 = new Thread(new TicketSystem(), "3");
Thread t4 = new Thread(new TicketSystem(), "4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
总结
-
这里关于JMM并没有深究,单纯是从使用的角度来看的,所以happens-before并没有展开讲,后续有时间在好好讲讲!
-
后续还有AtomicInteger实现安全递增和ReentranLock两种方式,以及线程池,明天在学吧。
-
后续对应的题目分别是
- 假设有三个线程,如何保证三个线程按照顺序执行,每一个线程都是在前一个线程执行完毕后在继续执行。
- 多种方式实现线程交叉打印12A34B56C,多种实现,主要是使用reentranlock
- 批次任务执行
-
对于我这种快速入门,纯靠背的人来说,最怕的就是这种具体的编程题,如果你让我做算法,完全没有问题,但是就是这种偏向于具体某种业务实现的,真的很害怕!害怕归害怕,还是得练!
-
这一类题目还是很好的实现的
- 确定每一个线程应该干什么,最好写成一个函数
- 确定多个线程之间协同的变量,声明为静态变量,在同步方法块中操作。